埋點統(tǒng)計在我們業(yè)務(wù)里經(jīng)常有遇到,或者很普遍的,我們自己網(wǎng)站也會加入第三方統(tǒng)計,我們會看到動態(tài)加載方式去加載jsdk,也就是你常??吹降膇nsertBefore操作,我們很少考慮到為什么這么做,直接同步加載不行嗎?
統(tǒng)計代碼會影響業(yè)務(wù)首屏加載嗎?同步引入方式,當然會,我的業(yè)務(wù)代碼還沒加載,首屏就加載一大段統(tǒng)計的jsdk,在移動端頁面打開要求比較高的苛刻條件下,首屏優(yōu)化,你可以在埋點統(tǒng)計上做些優(yōu)化,那么頁面加載會有一個很大的提升,本文是一篇筆者關(guān)于埋點優(yōu)化的筆記,希望看完在項目中有所思考和幫助。
最近遇到一個問題,先看一段代碼。
<!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>埋點</title>
<script>
window.formateJson = (data) => JSON.stringify(data, null, 2);
</script>
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
scriptDom.onload = function () {
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
};
setTimeout(() => {
console.log("setTimeout---444---");
head.parentNode.insertBefore(scriptDom, head);
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
<script async defer src="./js/app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
我們會發(fā)現(xiàn),打印的順序結(jié)果是下面這樣的:
---111---
app.js:2 ---333--- start load app.js
app.js:4 [
{
"id": "pink"
}
]
(index):30 setTimeout---444---
(index):26 ---2222---
(index):27 [
{
"id": "pink"
},
{
"id": "maic"
},
{
"id": "Tom"
}
]
冥思苦想,我們發(fā)現(xiàn)最后actd的結(jié)果是:
[
{
"id": "pink"
},
{
"id": "maic"
},
{
"id": "Tom"
}
]
其實我本意想要的結(jié)果是先添加maic,Tom,最后添加pink,需求就是,必須先在這個ts.js執(zhí)行后,預(yù)先添加基礎(chǔ)數(shù)據(jù),然后在其他業(yè)務(wù)app.js添加其他數(shù)據(jù),所以此時,無論如何都滿足不了我的需求。
試下想,為什么沒有按照我預(yù)期的要求走,問題就是出現(xiàn)在這個onload方法上。
onload事件
于是查詢資料尋得,onload事件是會等引入的外部資源加載完畢后才會觸發(fā)。
外部資源加載完畢是什么意思?
舉個栗子,我在引入的index2.html引入index2.js,然后在引入腳本上寫一個onload事件測試loadIndex2方法是否在我延時加載后進行調(diào)用的。
<!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>Document</title>
</head>
<body>
<script>
function loadIndex2() {
console.log("script loader...");
}
</script>
<script src="./js/index2.js" onload="loadIndex2()"></script>
</body>
</html>
index2.js中寫入一段代碼:
var startTime = Date.now()
const count = 1000;
let wait = 10000;
/// 設(shè)置延時
const time = wait * count;
for (let i = 0; i < time; i++) { }
var endTime = Date.now()
console.log(startTime, endTime)
console.log(`延遲了:${Math.ceil((endTime - startTime) / 1000)}s后執(zhí)行的`)
最后看下打印結(jié)果。

所以可以證實,onload是會等資源下載完了后,才會立即觸發(fā)。
所以我們回頭來看。
在瀏覽器的事件循環(huán)中,同步任務(wù)主線程肯定優(yōu)先會先順序執(zhí)行。
從打開印---111---,
然后到onload此時不會立即執(zhí)行。
遇到定時器,定時器設(shè)置了1s后會執(zhí)行,是個宏任務(wù),會放入隊列中,此時不會立即執(zhí)行。
然后接著會執(zhí)行 <script async defer src="./js/app.js"></script>腳本。
所以此時,執(zhí)行該腳本后,我們可以看到會先執(zhí)行push方法。
所以我們看到pink就最先被推入數(shù)組中,當該腳本執(zhí)行完畢后,此時會去執(zhí)行定時器。
定時器里我們看到我們插入方式insertBefore,當插入時成功時,此時會調(diào)用onload方法,所以此時就會添加maic與Tom。
很明顯,我們此時的需求不滿足我們的要求,而且一個onload方法已經(jīng)成了攔路虎。
那么我去掉onload試試,因為onload方法只會在腳本加載完畢后去執(zhí)行,他只會等執(zhí)行定時器后,成功插入腳本后才會真正執(zhí)行,而此時其他腳本已經(jīng)優(yōu)先它的執(zhí)行了。
那該怎么解決這個問題呢?
我把onload去掉試試,于是我改成了下面這樣:
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
setTimeout(() => {
console.log("setTimeout---444---");
head.parentNode.insertBefore(scriptDom, head);
}, 1000);
})
(window, document.getElementsByTagName("head")
[0], "actd", "script");
</script>
去掉onload后,我確實達到了我想要的結(jié)果。
最后的結(jié)果是:
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
}
]
但是你會發(fā)現(xiàn):

我先保證了window.actd添加了我預(yù)定提前添加的基礎(chǔ)信息,但此時,這個腳本并沒有真正添加到dom中,我們執(zhí)行完同步任務(wù)后,就會執(zhí)行app.js,當1s后,我才真正執(zhí)行了這個插入的腳本,而且我統(tǒng)計腳本你會發(fā)現(xiàn)此時是先執(zhí)行了app.js再加載tj.js的。
當執(zhí)行setTimeout時,我們會發(fā)現(xiàn)先執(zhí)行了內(nèi)部腳本,然后才執(zhí)行打印。
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
setTimeout(() => {
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
最后的結(jié)果,可以看到是這樣的:
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
},
{
"id": "setTimeout"
}
]
看到這里不知道你心里有沒有一個疑問,為什么在動態(tài)插入腳本時,我要用一個定時器1s鐘?為什么我需要用insertBefore這種方式插入腳本?,我同步方式引入不行嗎?不要定時器又會有什么樣的結(jié)果?
我們通常在接入第三方統(tǒng)計時,貌似都是一個這樣一個insertBefore插入的jsdk方式(但是一般我們都是同步方式引入jsdk)。
沒有使用定時器(3237ms)
<script async defer>
(function (win, head, attr, script) {
...
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>

結(jié)果:
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "setTimeout"
},
{
"id": "pink"
},
]
使用用定時器的(1622ms)
<script async defer>
(function (win, head, attr, script) {
...
setTimeout(() => {
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>

當我們用瀏覽器的Performance去比較兩組數(shù)據(jù)時,我們會發(fā)現(xiàn)總長時間,使用定時器的性能大概比沒有使用定時器的性能時間上大概要少50%,在summary中所有數(shù)據(jù)均有顯示的提升。
不經(jīng)感嘆,就一個定時器這一點點的改動,對整個應(yīng)用提升有這么大的提升,我領(lǐng)導(dǎo)說,快應(yīng)用在線加載時,之前因為這個統(tǒng)計js的加載明顯阻塞了業(yè)務(wù)頁面打開速度,做了這個優(yōu)化后,打開應(yīng)用顯著提升不少。
我們再繼續(xù)上一個問題,為什么不同步加載?
我把代碼改造一下,去除了一些無關(guān)緊要的代碼:
<!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>js執(zhí)行的順序問題</title>
<script>
window.formateJson = (data) => JSON.stringify(data, null, 2);
</script>
<script async defer src="./js/tj.js"></script>
<script async defer>
(function (win, head, attr, script) {
win[attr] = win[attr] || [];
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
<script async defer src="./js/app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
結(jié)果
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
}
]
嘿,需求是達到了,因為我的業(yè)務(wù)app.js加的數(shù)據(jù)是最后一條,說明業(yè)務(wù)功能上是ok的,但是我們看下分析數(shù)據(jù)。
首先肯定是加載順序會發(fā)生變化,會先加載tj.js然后再加載業(yè)務(wù)app.js,你會發(fā)現(xiàn)同步加載這種方式有個弊端,假設(shè)tj.js很大,那么是會阻塞影響頁面首屏打開速度的,所以在之前采用異步,定時器方式,首屏加載會有顯著提升。
同步加載(1846ms)


我們發(fā)現(xiàn)tj.js與app.js相隔的時間很少,且我們從火焰圖中分析看到,Summary的數(shù)據(jù)是1846ms。
綜上比較,雖然同步加載依然比不上使用定時器的加載方式,使用定時器相比較同步加載,依然是領(lǐng)先11%左右。
異步標識async/defer
在上面的代碼中,我們多次看到async和defer標識,在之前文章中筆者有寫過一篇你真的了解esModule嗎,闡述一些關(guān)于script標簽中type="moudle", defer,async的幾個標識,今天再次回顧下。
其實從腳本優(yōu)先級來看,同步的永遠優(yōu)先最高,當一個script標簽沒有指定任何標識時,此時根據(jù)js引擎執(zhí)行來說,誰放前面,誰就會優(yōu)先執(zhí)行,前面沒執(zhí)行完,后面同步的script就不會執(zhí)行。
注意到?jīng)]有,我在腳本上有加async與defer。
在上面栗子中,我們使用insertBefore方式,這就將該插入的js腳本的優(yōu)先級降低了。
我們從上面火焰圖中可以分析得處結(jié)論,排名先后順序依次如下:
1、setTimeout+insertBefore
執(zhí)行順序:app.js->tj.js
2、同步腳本加載
執(zhí)行順序:tj.js->app.js
3、不使用定時器+insertBefore
執(zhí)行順序:app.js->tj.js
當我們知道在1中,app.js優(yōu)先于tj.js
因為insertBefore就是一種異步動態(tài)加載方式
舉個例子:
<script async defer>
// 執(zhí)行
console.log(1)
// 2 insertBefore 這里再動態(tài)添加js
</script>
<script async defer>
// 執(zhí)行
console.log(3)
</script>
執(zhí)行關(guān)系就是1,3,2。
關(guān)于async與defer誰先執(zhí)行時,defer的優(yōu)先級比較低,會等異步標識的async下載完后立馬執(zhí)行,然后再執(zhí)行defer的腳本,具體可以參考以前寫的一篇文章你真的了解esModule嗎。
總結(jié)
- 統(tǒng)計腳本,我們可以使用定時器+insertBefore方式可以大大提高首屏的加載速度,這也給我們了一些啟發(fā),首屏加載,非業(yè)務(wù)代碼,比如埋點統(tǒng)計可以使用該方案做一點小優(yōu)化加快首屏加載速度。
- 如果使用insertBefore方式,非常不建議同步方式insertBefore,這種方式還不如同步加載統(tǒng)計腳本。
- 在特殊場景下,我們需要加載統(tǒng)計腳本,有基礎(chǔ)信息的依賴后,我們也需要在業(yè)務(wù)代碼使用統(tǒng)計,我們不要在動態(tài)加載腳本的同時使用onload,在onload中嘗試添加基礎(chǔ)信息,實際上這種方式并不能滿足你的需求。
- 一些關(guān)于async與defer的特性,記住,執(zhí)行順序,同步任務(wù)會優(yōu)先執(zhí)行,async是異步,腳本下載完就執(zhí)行,defer優(yōu)先級比較低。
- 本文示例code example[1]
[1]code example: https://github.com/maicFir/lessonNote/tree/master/javascript/21-js異步執(zhí)行