近年來,Instagram發佈了許多功能-咱們推出了故事,過濾器,建立工具,通知和消息直遞,以及許多其餘功能和優化。 可是,隨着產品功能的增加,一個不幸的反作用是咱們的網絡性能開始降低。 在過去的一年中,咱們有意識地努力來改善這一情況。 到目前爲止,咱們的不懈努力已使Feed頁的加載時間累計提高了近50%。 這一系列博客文章將概述咱們爲實現這些改進所作的一些工做。前端
在第1部分中,咱們介紹了數據和資源預加載,在第2部分中,咱們介紹了經過直接向客戶端推送數據而不是等待客戶端請求數據來提升性能,在第3部分中,咱們介紹了緩存優先渲染。瀏覽器
在第1-3部分中,咱們介紹了各類方法,這些方法能夠優化關鍵路徑的靜態資源和數據查詢的加載模式。 可是,還有一個咱們還沒有涉及的關鍵領域,對於提升Web應用程序的性能相當重要,尤爲是在低端設備上-向用戶交付更少的代碼,尤爲是更少的JavaScript。緩存
這看起來彷佛很明顯,可是這裏有幾點須要考慮。 業界廣泛認爲,經過網絡下載的JavaScript的大小是重要的(即壓縮後的大小),可是咱們發現真正重要的是壓縮前的大小,由於即便在本地緩存,也須要在用戶設備上進行解析和執行。bash
若是您的站點有不少重複用戶(較高的瀏覽器緩存命中率)或在移動設備上訪問您的站點的用戶,則尤爲如此。 在這些狀況下,JavaScript在CPU上的解析和執行性能成爲主要的限制因素,而不是網絡下載時間。服務器
例如,當咱們爲JavaScript資產實施Brotli壓縮時,咱們發現整個網絡的壓縮後大小減小了近20%,可是從最終用戶的角度來看,整體頁面加載時間沒有統計上的顯著減小。網絡
另外一方面,咱們發現壓縮前JavaScript尺寸的減少始終能夠提升性能。 在關鍵路徑上執行的JavaScript和主頁完成後動態導入的JavaScript之間也應加以區分。async
理想狀況下,減小應用程序中的JavaScript總量雖然很不錯,但短時間內要進行優化的關鍵是關鍵路徑上執行的JavaScript數量(咱們使用稱爲「每條路由的關鍵字節數」的指標進行跟蹤) )。模塊化
延遲加載的動態導入JavaScript一般不會對頁面加載性能產生重大影響,所以,將不可見或與交互相關的UI組件從初始頁面包中移出並動態導入包是一種有效的策略。函數
從長遠來看,重構咱們的UI以減小關鍵路徑上的腳本數量對於提升性能相當重要,但這是一項艱鉅的任務,須要時間。 在短時間內,咱們進行了許多項目,以對產品開發人員透明的方式提升現有代碼的大小和執行效率,而且幾乎不須要重構現有產品代碼。工具
咱們使用Metro(與React Native使用的捆綁器)打包前端Web資產,所以咱們能夠直接訪問內聯引用。 內聯需求將需求/導入模塊的成本在實際使用時首次轉移。
這意味着您能夠避免爲未使用的功能付出執行成本(儘管您仍將支付下載和解析它們的成本),而且能夠在應用程序啓動時更好地攤銷執行成本,而不是進行大量的前期計算。
const config = {
transformer: {
getTransformOptions: () => {
return {
transform: { inlineRequires: true },
};
},
},
};
module.exports = config;
複製代碼
咱們能夠看下下面這個例子:
const foo = require('foo');
const bar = require('bar');
module.exports = function baz() {
foo();
}
複製代碼
使用內聯引用能夠將其轉換爲以下所示(您能夠經過在瀏覽器開發人員工具的Instagram JS源代碼中搜索r(d[來找到這些內聯要求))
module.exports = function baz() {
require('foo')();
}
複製代碼
如咱們所見,它其實是經過將對所需模塊的本地引用替換爲須要該模塊的函數調用而起做用的。 這意味着除非實際使用該模塊中的代碼,不然永遠不須要該模塊(所以也就不會執行該模塊)。 在大多數狀況下,這很是有效,可是要注意會致使問題的一些極端狀況,即具備反作用的模塊。 例如:
// Module A
window.globalState = { 'foo': 'bar' };
// Module B
module.exports = function() {
console.log(window.globalState);
}
// Module C
const A = require('A');
const B = require('B');
B();
複製代碼
沒有內聯引用,模塊C將輸出{'foo':'bar'},可是當咱們啓用內聯引用時,它將輸出undefined,由於B對A具備隱式依賴性。這是一我的爲的示例,但還有其餘示例。
在現實世界中,這種狀況可能會產生影響,例如,若是模塊在其初始化過程當中進行了一些日誌記錄,該怎麼辦-啓用內聯請求可能致使該日誌記錄中止發生。
經過linters來檢查在模塊做用域級別當即執行的代碼,這在大多數狀況下是能夠避免的,可是咱們必須今後優化中將某些文件列入黑名單,例如須要當即執行的運行時polyfills。 在整個代碼庫中嘗試啓用內聯需求以後,咱們發現Feed TTI(互動時間)和Display Done分別提升了12%和9.8%,並認爲處理這些次要狀況對於提升性能是值得的。
推進採用諸如Babel之類的編譯器/編譯器工具的主要驅動器之一,是容許開發人員使用現代的JavaScript編碼習慣,同時在缺少支持的瀏覽器中能夠運行。
從那時起,出現了這些工具的許多其餘重要場景,包括諸如Typescript和ReasonML之類的JS編譯語言,諸如JSX和Flow類型註釋之類的語言擴展以及針對諸如國際化之類的時間AST操縱。
所以,這個額外的編譯步驟不太可能很快在前端開發工做流程中消失。不過,話雖如此,但值得回顧的是,在2019年是否仍然有達到此目的的原始目的(跨瀏覽器兼容性)。
大多數主要瀏覽器的最新版本如今都很好地支持ES2015和更新功能(例如async / await),所以絕對有可能直接提供包含這些更新功能的JavaScript -可是咱們必須首先回答兩個關鍵問題:
要回答第一個問題,咱們首先必須肯定要在不進行轉譯的狀況下提供哪些功能,以及咱們要爲不一樣的瀏覽器支持多少個構建變體。 咱們選擇了兩個版本,一個版本須要支持ES2017語法,另外一個版本能夠移植回ES5(此外,咱們還添加了一個可選的polyfill捆綁包,該捆綁包僅適用於缺乏運行時支持的舊版瀏覽器。 最近的DOM API)。
經過在服務器端進行一些基本的用戶代理嗅探來檢測對這些組的支持,從而確保從客戶端檢測要加載的捆綁軟件起,沒有運行時成本或額外的往返時間。
考慮到這一點,咱們肯定了instagram.com的56%的用戶能夠在不進行任何代碼轉換或運行時polyfill的狀況下得到ES2017版本的服務,而且考慮到該百分比只會隨着時間的推移而增長– 考慮到可以使用它的用戶數量,彷佛值得支持兩個版本。
至於第二個問題-直接交付ES2017的性能優點是什麼-首先讓咱們看一下Babel在將一些常見的構造轉換回ES5方面的實際做用。 左列是ES2017代碼,右列是已編譯的ES5兼容版本。
Class (ES2017 vs ES5)
Async/Await (ES2017 vs ES5)
Arrow functions (ES2017 vs ES5)
Destructuring assignment (ES2017 vs ES5)
從中咱們能夠看到,在編譯這些語法時有至關大的開銷(即便您在較大的代碼庫上分攤了某些運行時幫助程序函數的成本)。 對於Instagram,當咱們從構建中刪除全部ES2017轉碼插件時,咱們看到核心JavaScript包的大小減小了5.7%。
在測試中,咱們發現使用ES2017捆綁包的用戶與未使用ES2017捆綁包的用戶相比,提要頁面的端到端加載時間縮短了3%。
儘管到目前爲止取得的進展使人印象深入,但到目前爲止,咱們所作的工做只是開始。 在Redux存儲/ Reducer模塊化,更好的代碼拆分,將更多JavaScript執行移出關鍵路徑,優化滾動性能,適應不一樣的帶寬條件等方面,還有大量的改進空間。
『奶爸碼農』從事互聯網研發工做10+年,經歷IBM、SAP、陸金所、攜程等國內外IT公司,目前在美團負責餐飲相關大前端技術團隊,按期分享關於大前端技術、投資理財、我的成長的思考與總結