前端性能優化篇之console.log

前言

前端程序員幾乎天天不是在打log就是在打log的路上,最近就在本身真實項目中遇到了一個console.log的坑,由此記錄一下console.log過程當中你須要瞭解的知識,少走彎路。javascript

console.log真的是異步嗎?

不少人在開發過程當中利用console.log調試都被它「騙過」,在監視一些複雜對象的時候,log呈現給咱們的值可能與預期不符,以下:
image-20210613000954349
這時有人直接下定義:console.log是異步的!其實這個問題在《你不知道的javascript中卷》第二部分異步和性能1.1節異步控制檯部分有說起:前端

並無什麼規範或一組需求指定console.* 方法族如何工做——它們並非JavaScript 正式的一部分,而是由宿主環境(請參考本書的「類型和語法」部分)添加到JavaScript 中的。所以,不一樣的瀏覽器和JavaScript 環境能夠按照本身的意願來實現,有時候這會引發混淆。
尤爲要提出的是,在某些條件下,某些瀏覽器的console.log(..) 並不會把傳入的內容當即輸出。出現這種狀況的主要緣由是,在許多程序(不僅是JavaScript)中,I/O 是很是低速的阻塞部分。因此,(從頁面/UI 的角度來講)瀏覽器在後臺異步處理控制檯I/O 可以提升性能,這時用戶甚至可能根本意識不到其發生。

咱們得知道一點,JS中對象是引用類型,每次使用對象時,都只是使用了對象在堆中的引用。vue

當咱們在使用a.b.c = 2改變了對象的屬性值時,它在堆中c的值也變成了2,而當咱們不展開對象看的時候,console.log打印的是對象當時的快照,因此咱們看到的c屬性值是沒改變以前的1,展開對象時,它實際上是從新去內存中讀取對象的屬性值,因此當咱們展開對象後看到的c屬性值是2。java

結論1

因而可知,console.log打印出來的內容並非必定百分百可信的內容。通常對於基本類型numberstringbooleannullundefined的輸出是可信的。但對於Object等引用類型來講,則就會出現上述異常打印輸出。
若是改成console.log(JSON.stringfy(obj))則會打印出當前對象的快照,也就能拿到相似同步下的理想結果。更好的解決方案是使用斷點進行調試,在那裏執行徹底中止,您能夠檢查每一個點的當前值。僅對可序列化和不可變數據使用日誌記錄。node

console.log會影響性能嗎?

經過上面的問題,能夠反思一下:爲何你能夠在控制檯打印程序變量的快照?而且是引用類型的值時,展開對象會從新去內存讀取對象的值?程序運行完後難道變量不該該被gc回收嗎?python

在傳遞給console.log的對象是不能被垃圾回收 ♻️,由於在代碼運行以後須要在開發工具能查看對象信息。因此最好不要在生產環境中console.log任何對象。git

案例一

能夠在控制檯印證這一點:程序員

var Foo = function() {};
console.log(new Foo());

打開控制檯Memory調試:github

image-20210613001444145

案例二

<script>
...

mounted () {
    this.handleSignCallback({
      type: SIGNATURE_TYPES.SIGNATURE_GET_SIGN_STATUS,
      success: true,
      result: {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
        }
      }
    });
},
methods: {
  handleSignCallback(data) {
    const { type, result, success } = data;
    // do something...
    console.log(result) // 返回結果打印
    if (true) { // 真實項目中用來判斷是否知足條件
      this.timer = setTimeout(() => {
      this.handleSignCallback({
        type: SIGNATURE_TYPES.SIGNATURE_GET_SIGN_STATUS,
        success: true,
        result: {
          longStr: new Array(1000000).join('*'),
          someMethod: function () {
          }
        }
      })
      clearTimeout(this.timer);
    }, 1000)
   }
  }
}
</script>

真實項目在輪詢中須要判斷某些邏輯,知足條件了,會延遲幾秒繼續去請求後臺,處理一些數據,直到返回的對象沒有知足條件的數據才中止去請求拉數據,期間我想打印返回的數據,因而我加了console.log(result),可是若是數據量大的話,執行一會就會程序崩潰,打開調試控制檯->Memory:npm

image-20210613002808312

js Heap在一直上升,無上限,直至瀏覽器崩潰;

可是當我註釋console.log(result)這行 時,再次觀察堆內存狀況:

image-20210613003410126

這時堆內存使用狀況基本穩定在150左右。

結論2

先給上述問題下個結論:console.log確實會影響網頁性能。原因請結合下述展開問題結合深究。

開發小技巧

針對結論二,你們應該都知道了,生產模式下儘可能不要留console.log,也許你們如今公司裏可能都是這麼作的,可是你真的知道爲何這麼作嗎?下面舉例Vue項目如何統一去除坑爹的console.log:

  1. 安裝babel-plugin-transform-remove-console

    npm i babel-plugin-transform-remove-console --dev

  2. 配置babel.config.js插件

    // babel.config.js文件
    const plugins = []
    // 生產環境移除console
    if(process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') {
      plugins.push("transform-remove-console")
    }
    module.exports = {
      presets: [
        '@vue/app'
      ],
      plugins: [...plugins]
    }

結合案例二作性能調試

這裏結合Chrome的Devtools–>Performance作一些分析,操做步驟以下:

  1. 開啓Performance的記錄
  2. 執行CG按鈕,建立基準參考線
  3. 屢次點擊【click】按鈕,新建Leaker對象
  4. 執行CG按鈕
  5. 中止記錄這裏結合Chrome的Devtools–>Performance作一些分析,操做步驟以下:

    1. 開啓Performance的記錄
    2. 執行CG按鈕,建立基準參考線
    3. do something or create Object
    4. 執行CG按鈕
    5. 中止記錄

clipboard.pngclipboard.png

開啓log模式下:

image-20210613081509239

說明:測試代碼因爲模擬輪詢場景,定時器觸發頻繁,可能出現gc的時間點js Heap高於基準線一點,主要看最後一次的js Heap跟第一次觸發的線是否是差很少持平

能夠看到這裏的js Heap(藍色線,綠色線是nodes節點數量)一直在升高,而且我手動回收垃圾(gc)也無濟於事 ,很明顯,發生了內存泄露!

關閉log模式下:

image-20210613082254800

關閉log後,觀察js堆內存狀況如上:能夠看到,手動GC後兩個時間點,js Heap基本恢復到同一水平線。

在普通場景下測試,例如開啓記錄->gc->點擊事件,建立對象(N遍)->gc->中止記錄,js Heap兩個gc時間點基本重合。

內存泄露排查方法二

Heap Profiling能夠記錄當前的堆內存(heap)的快照,並生成對象的描述文件,該描述文件給出了當時JS運行所用的全部對象,以及這些對象所佔用的內存大小、引用的層級關係等等。

JS運行的時候,會有棧內存(stack)和堆內存(heap),當咱們new一個類的時候,這個new出來的對象就保存在heap裏,而這個對象的引用則存儲在stack裏。程序經過stack的引用找到這個對象。例如:var a = [1,2,3],a是存儲在stack中的引用,heap裏存儲着內容爲[1,2,3]的Array對象。

打開調試工具,點擊Memory中的Profiles標籤,選中「Take Heap Snapshot」,點擊「start」按鈕,就能夠拍在當前JS的heap快照了。

img

右邊視圖中列出了heap裏的對象列表。

constructor:類名
Distance:對象到根的引用層級距離
Objects Count:該類的對象數
Shallow Size:對象所佔內存(不包含內部引用的其餘對象所佔的內存)
Retained Size:對象所佔的總內存(包含····················································)
點擊上圖左上角的黑圈圈,會出現第二個內存快照
img

將上圖框框切換到comparison(對照)選項,該視圖列出了當前視圖與上一個視圖的對象差別

New:新建了多少對象
Deleted:回收了多少對象
Delta:新建的對象個數減去回收的對象個數
重點看closure(閉包),若是#Delta爲正數,則表示建立了閉包函數,若是多個快照中都沒有變負數,則表示沒有銷燬閉包。

有趣(無聊)的問題

while (true)
    console.log("hello... there");

運行以上代碼,在大約 3 分鐘左右的時間內,節點消耗了 1.5GB 的內存。

有人會說,死循環了能不引發內存飆升嗎?

那看下面這個:

def hello():
    while True:
        print "hello world..."

hello()

Python 永遠保持在 3.3MB。

有人可能會想到前面說的,console.log會引發內存泄露,可是前面說了是引用類型會形成內存泄露,這裏直接打印字符串,爲何性能也會這麼差?

簡短回答:控制檯輸出是緩衝和異步的。而且除了全局內存限制外,緩衝區不受任何限制。所以,它填滿緩衝區並死亡。
更多信息在#1741

「緩衝區」是指其實是異步操做隊列的隱式緩衝區。

  1. 鏈接的檢查器客戶端將致使console.log()緩衝有限數量的消息
  2. 當您打印的速度比接收器(tty、管道、文件等)可使用它們的速度快時,Libuv 能夠緩衝消息。這是您能夠經過調整寫入速度來解決的問題。
Libuv Libuv是一個高性能的,事件驅動的異步I/O庫,它自己是由C語言編寫的,具備很高的可移植性。

結論3

node開發時,在咱們寫入大型日誌條目的狀況下,寫入文件進行日誌記錄是迄今爲止最好的方法。服務稍後讀取日誌文件並將其聚合到適當的服務。

相關文章
相關標籤/搜索