作者:京東科技 孫凱
一、前言
相信很多前端開發者在做專案時同時也都做過頁面性能優化,這不單是前端的必備職業技能,也是考驗一個前端基礎是否扎實的考點,而性能指標也通常是每一個開發者的績效之一,尤其馬上接近年關,頁面白屏時間是否過長、首屏加載速度是否達標、影片是否能流暢運行,諸如此類關于性能更具體的指標和感受,很可能也是決定著年底你能拿多少年終獎回家過年的晴雨表,
關于性能優化,我們一般從以下四個方面考慮:
-
開發時性能優化
-
編譯時性能優化
-
加載時性能優化
-
運行時性能優化
而本文將從第三個方面展開,講一講哪些因素將影響到頁面加載總時長,談到總時長,那總是避免不了要談及window.onload,這不但是本文的重點,也是常見頁面性能監控工具中必要的API之一,如果你對自己頁面加載的總時長不滿意,歡迎讀完本文后在評論區交流,
二、關于 window.onload
這個掛載到window上的方法,是我剛接觸前端時就掌握的技能,我記得尤為深刻,當時老師說,“對于初學者,只要在這個方法里寫邏輯,一定沒錯兒,它是整個檔案加載完畢后執行的生命周期函式”,于是從那之后,幾乎所有的練習demo,我都寫在這里,也確實沒出過錯,
在MDN上,關于onload的解釋是這樣的:load 事件在整個頁面及所有依萊澩如樣式表和圖片都已完成加載時觸發,它與DOMContentLoaded不同,后者只要頁面 DOM 加載完成就觸發,無需等待依萊澩的加載,該事件不可取消,也不會冒泡,
后來隨著前端知識的不斷擴充,這個方法后來因為有了“更先進”的DOMContentLoaded,在我的代碼里而逐漸被替代了,目前除了一些極其特殊的情況,否則我幾乎很難用到window.onload這個API,直到認識到它影響到頁面加載的整體時長指標,我才又一次拾起來它,
三、哪些因素會影響 window.onload
本章節主要會通過幾個常用的業務場景展開描述,但是有個前提,就是如何準確記錄各種型別資源加載耗時對頁面整體加載的影響,為此,有必要先介紹一下前提,
為了準確描述資源加載耗時,我在本地環境啟動了一個用于資源請求的node服務,所有的資源都會從這個服務中獲取,之所以不用遠程服務器資源的有主要原因是,使用本地服務的資源可以在訪問的資源鏈接中設定延遲時間,如訪問腳本資源http://localhost:3010/index.js?delay=300,因鏈接中存在delay=300,即可使資源在300毫秒后回傳,這樣即可準確控制每個資源加載的時間,
以下是node資源請求服務延遲相關代碼,僅僅是一個中間件:
const express = require("express")
const app = express()
app.use(function (req, res, next) {
Number(req.query.delay) > 0
? setTimeout(next, req.query.delay)
: next()
})
-
場景一: 使用 async 異步加載腳本場景對 onl oad 的影響
示例代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <!-- 請求時長為1秒的js資源 --> <script src="http://localhost:3010/index.js?delay=1000" async></script> </head> <body> </body> </html>瀏覽器表現如下:

通過上圖可以看到,瀑布圖中深藍色豎線表示觸發了DOMContentLoaded事件,而紅色豎線表示觸發了window.onload事件(下文中無特殊情況,不會再進行特殊標識),由圖可以得知使用了 async 屬性進行腳本的異步加載,仍會影響頁面加載總體時長, -
場景二:使用 defer 異步加載腳本場景對 onl oad 的影響
示例代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <!-- 請求時長為1秒的js資源 --> <script src="http://localhost:3010/index.js?delay=1000" defer></script> </head> <body> </body> </html>瀏覽器表現如下:

由圖可以得知使用了 defer 屬性進行腳本的異步加載,除了正常的在DOMContentLoaded之后觸發腳本執行,也影響頁面加載總體時長, -
場景三:異步腳本中再次加載腳本,也就是常見的動態加載腳本、樣式資源的情況
html 代碼保持不變,index.js內示例代碼:const script = document.createElement('script') // 請求時長為0.6秒的js資源 script.src = 'http://localhost:3010/index2.js?delay=600' script.onload = () => { console.log('js 2 異步加載完畢') } document.body.appendChild(script)結果如下:

從瀑布圖可以看出,資源的連續加載,導致了onload事件整體延后了,這也是我們再頁面中非常常見的一種操作,通常懶加載一些不重要或者首屏外的資源,其實這樣也會導致頁面整體指標的下降,不過值得強調的一點是,這里有個有意思的地方,如果我們把上述代碼進行改造,洗掉最后一行的
document.body.appendChild(script),發現 index2 的資源請求并沒有發出,也就是說,腳本元素不向頁面中插入,腳本的請求是不會發出的,但是也會有反例,這個我們下面再說,在本示例中,后來我又把腳本請求換成了 css 請求,結果是一致的,
-
場景四:圖片的懶加載/預加載
html 保持不變,index.js 用于加載圖片,內容如下:const img = document.createElement('img') // 請求時長為0.5秒的圖片資源 img.src = 'http://localhost:3010/index.png?delay=500' document.body.appendChild(img)結果示意:

表現是與場景三一樣的,這個不再多說,但是有意思的來了,不一樣的是,經過測驗發現,哪怕洗掉最后一行代碼:document.body.appendChild(img),不向頁面中插入元素,圖片也會發出請求,也同樣延長了頁面加載時長,所以部分同學就要注意了,這是一把雙刃劍:當你真的需要懶加載圖片時,可以少寫最后一行插入元素的代碼了,但是如果大量的圖片加載請求發出,哪怕不向頁面插入圖片,也真的會拖慢頁面的時長,趁著這個場景,再多說一句,一些埋點資料的上報,也正是借著圖片有不需要插入dom即可發送請求的特性,實作成功上傳的,
-
場景五:普通介面請求
html 保持不變,index.js 內容如下:// 請求時長為500毫秒的請求介面 fetch('http://localhost:3010/api?delay=500')結果如下圖:

可以發現普通介面請求的發出,并不會影響頁面加載,但是我們再把場景弄復雜一些,見場景六, -
場景六:同時加載樣式、腳本,腳本加載完成后,內部http介面請求,等請求結果回傳后,再發出圖片請求或修改dom,這也是更貼近生產環境的真實場景
html 代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <!-- 請求時長為1.2秒的css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1200"> <!-- 請求時長為0.4秒的js --> <script src="http://localhost:3010/index.js?delay=400" async></script> </head> <body> </body> </html>index.js 代碼:
async function getImage () { // 請求時長為0.5秒的介面請求 await fetch('http://localhost:3010/api?delay=500') const img = document.createElement('img') // 請求時長為0.5秒的圖片資源 img.src = 'http://localhost:3010/index.png?delay=500' document.body.appendChild(img) } getImage()結果圖如下:

如圖所示,結合場景五記的結果,雖然普通的 api 請求并不會影響頁面加載時長,但是因為api請求過后,重新請求了圖片資源(或大量操作 dom),依然會導致頁面加載時間變長,這也是我們日常開發中最常見的場景,頁面加載了js,js發出網路請求,用于獲取頁面渲染資料,頁面渲染時加載圖片或進行dom操作,
-
場景七:頁面多媒體資源的加載
示例代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> </head> <body> <video src="http://localhost:3010/video.mp4?delay=500" controls></video> </body> </html>結果如圖:

對于視頻這種多媒體資源的加載比較有意思,video 標簽對于資源的加載是默認開啟 preload 的,所以資源會默認進行網路請求(如需關閉,要把 preload 設定為 none ),可以看到紅色豎線基本處于圖中綠色條和藍色條中間(實際上更偏右一些),圖片綠色部分代表資源等待時長,藍色部分代表資源真正的加載時長,且藍色加載條在onload的豎線右側,這說明多媒體的資源確實影響了 onl oad 時長,但是又沒完全影響,因為設定了500ms的延遲回傳資源,所以 onl oad 也被延遲了500ms左右,但一旦視頻真正開始下載,這段時長已經不記錄在 onl oad 的時長中了,
其實這種行為也算合理,畢竟多媒體資源通常很大,占用的帶寬也多,如果一直延遲 onl oad,意味著很多依賴 onl oad 的事件都無法及時觸發,
接下來我們把這種情況再復雜一些,貼近實際的生產場景,通常video元素是包含封面圖 poster 屬性的,我們設定一張延遲1秒的封面圖,看看會發生什么,結果如下:

不出意外,果然封面圖影響了整體的加載時長,魔鬼都在細節中,封面圖也需要注意優化壓縮, -
場景八:異步腳本和樣式資源一同請求
示例代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <!-- 請求時長為1秒的css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1000"> <!-- 請求時長為0.5秒的js --> <script src="http://localhost:3010/index.js?delay=500" async></script> </head> <body> </body> </html>瀏覽器表現如下:

可以看出 css 資源雖然沒有阻塞腳本的加載,但是卻延遲了整體頁面加載時長,其中原因是css資源的加載會影響 render tree 的生成,導致頁面遲遲不能完成渲染,
如果嘗試把 async 換成 defer,或者干脆使用同步的方式加載腳本,結果也是一樣,因結果相同,本處不再舉例, -
場景九:樣式資源先請求,再執行行內腳本邏輯,最后加載異步腳本
我們把場景八的代碼做一個改造,在樣式標簽和異步腳本標簽之間,加上一個只包含空格的行內腳本,讓我們看看會發生什么,代碼如下:<!DOCTYPE html> <html lang="en"> <head> <script> console.log('頁面js 開始執行') </script> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <!-- 請求時長為1秒的css --> <link rel="stylesheet" href="http://localhost:3010/index.css?delay=2000"> <!-- 此標簽僅有一個空格 --> <script> </script> <!-- 請求時長為0.5秒的js --> <script src="http://localhost:3010/index.js?delay=500" async></script> </head> <body> </body> </html>index.js 中的內容如下:
console.log("腳本 js 開始執行");結果如下,這是一張 GIF,加載可能有點慢:

這個結果非常有意思,他到底發生了什么呢?-
腳本請求是0.5秒的延遲,樣式請求是2秒
-
腳本資源是 async 的請求,異步發出,應該什么時候加載完什么時候執行
-
但是圖中的結果卻是等待樣式資源加載完畢后才執行
答案就在那個僅有一個空格的腳本標簽中,經反復測驗,如果把標簽換成注釋,也會出現一樣的現象,如果是一個完全空的標簽,或者根本沒有這個腳本標簽,那下方的index.js 通過 async 異步加載,并不會違反直覺,加載完畢后直接執行了,所以這是為什么呢?
這其實是因為樣式資源下方的 script 雖然僅有一個空格,但是被瀏覽器認為了它內部可能是包含邏輯,一定概率會存在樣式的修改、更新 dom 結構等操作,因為樣式資源沒有加載完(被延遲了2秒),導致同步 js (只有一個空格的腳本)的執行被阻塞了,眾所周知頁面的渲染和運行是單執行緒的,既然前面已經有了一個未執行完成的 js,所以也導致了后面異步加載的 js 需要在佇列中等待,這也就是為什么 async 雖然異步加載了,但是沒有在加載后立即執行的原因,
-
-
場景十:字體資源的加載
示例代碼:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test</title> <style> @font-face { font-family: font-custom; src: url('http://localhost:3010/font.ttf?delay=500'); } body { font-family: font-custom; } </style> </head> <body></body> </html>結果如下:

可以看到,此情況下字體的加載是對 onl oad 有影響的,然后我們又測驗了一下只宣告字體、不使用的情況,也就是洗掉上面代碼中 body 設定的字體,發現這種情況下,字體是不會發出請求的,僅僅是造成了代碼的冗余,
四、總結
前面列舉了大量的案例,接下來我們做個總結,實質性影響 onl oad 其實就是幾個方面,
-
圖片資源的影響毋庸置疑,無論是在頁面中直接加載,還是通過 js 懶加載,只要加載程序是在 onl oad 之前,都會導致頁面 onl oad 時長增加,
-
多媒體資源的等待時長會被記入 onl oad,但是實際加載程序不會,
-
字體資源的加載會影響 onl oad,
-
網路介面請求,不會影響 onl oad,但需要注意的是介面回傳后,如果此時頁面還未 onl oad,又進行了圖片或者dom操作,是會導致 onl oad 延后的,
-
樣式不會影響腳本的加載和決議,只會阻塞腳本的執行,
-
異步腳本請求不會影響頁面決議,但是腳本的執行同樣影響 onl oad,
五、優化舉措
-
圖片或其他資源的預加載可以通過 preload 或 prefetch 請求,這兩種方式都不會影響 onl oad 時長,
-
一定注意壓縮圖片,頁面中圖片的加載速度可能對整體時長有決定性影響,
-
盡量不要做串行請求,沒有依賴關系的情況下,推薦并行,
-
中文字體包非常大,可以使用字蛛壓縮、或用圖片代替,
-
靜態資源上 cdn 很重要,壓縮也很重要,
-
洗掉你認為可有可無的代碼,沒準哪一行代碼就會影響加載速度,并且可能很難排查,
-
視瞥澩如果在首屏以外,不要開啟預加載,合理使用視頻的 preload 屬性,
-
async 和 defer 記得用,很好用,
-
非必要的內容,可以在 onl oad 之后執行,是時候重新拾起來這個 api 了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/549444.html
標籤:Html/Css
上一篇:CSS常用背景屬性(背景顏色、背景圖片、背景平鋪、背景位置、背景附著、背景色半透明、背景屬性復合寫法)
下一篇:前端設計模式——MVVM模式
