原文地址:Doing Differential Serving in 2019
原文做者:Jeremy Wagne
譯文出自:FE-star/speed
譯者:smallbonelu
校對者:[]
本文連接:[]javascript
若是你正在閱讀本文,那麼你可能就是那種一直在尋找務實,前瞻性思惟方式來提升網站速度的人。因此,當我讀了由菲爾·沃爾頓寫的一個被稱爲差別化服務的指南後,我很感興趣。若是你尚未據說過這種技術,其實就是你能夠爲你的站點編譯和提供兩個獨立的JavaScript捆綁包:html
咱們使用Babel來轉換腳本,以便咱們能夠在任何地方使用它們,可是咱們這樣作會有一些危險。它在大多數配置中添加的額外代碼對於現代瀏覽器上的用戶來講一般不是必要的。經過一些努力,能夠更改構建過程,以減小咱們在現代瀏覽器上發送給大量用戶的代碼量,同時保持對遺留客戶端(例如IE11)的兼容性。差別化服務的目的不只僅是改善傳送時間 - 這確定對傳送時間有所幫助。它還能夠經過減小瀏覽器須要處理的腳本數量來幫助減小主線程的阻塞,這是一個資源密集型處理過程。java
在本指南中,你將瞭解如何在2019年的構建管道中設置差別化服務,從設置Babel到你須要在webpack中進行哪些調整,以及完成全部這些工做的好處。node
輸出同一應用程序的多個構建版本涉及每一個目標的Babel配置。畢竟,單個項目中的多個Babel配置並不罕見。一般經過將每一個單獨的配置對象放在env
對象鍵下來完成。如下是爲差別化服務設置的Babel配置中的內容:webpack
// babel.config.js
module.exports = {
env: {
// 這是咱們將用於爲舊版瀏覽器生成捆綁包的配置
legacy: {
presets: [
[
"@babel/preset-env", {
modules: false,
useBuiltIns: "entry",
// 這應該合理地針對舊瀏覽器.
targets: "> 0.25%, last 2 versions, Firefox ESR"
}
]
],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
]
},
// 這是用來爲現代瀏覽器生成包的配置.
modern: {
presets: [
[
"@babel/preset-env", {
modules: false,
targets: {
// 這將針對支持ES模塊的瀏覽器.
esmodules: true
}
}
]
],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
]
}
}
};
複製代碼
你會注意到有兩種配置:modern
和legacy
。這些都控制着Babel如何轉換每一個包。諷刺的是,將polyfills並添加沒必要要的變換到咱們的代碼的工具與咱們能夠用來發送更少代碼的是同一個工具。在Phil的原始文章中,他使用Babel 6 babel-preset-env
來實現這一目標。如今Babel 7發佈了,我改用了@babel/preset-env
。git
首先要注意的是,@babel/preset-env
每一個配置中使用的選項都不一樣。對於legacy
選項,咱們將browserslist查詢傳遞給適用於舊版瀏覽器的targets
選項。咱們也告訴preset包括來自@babel/polyfill
與useBuiltIns
選項的polyfills。除了presets,咱們還包括一些必要的插件。github
注意:useBuiltIns
除了false
以外還接受兩個值。這些值是*entry和usage*。文檔很好地解釋了它們的不一樣之處,但值得注意的是"usage"是實驗性的。與"entry"相比,**它一般會產生更小的包,但我發現我須要指定"entry"才能讓腳本在IE 11中運行。web
對於modern
配置而言,配置看起來基本相同,除了價值targets
不一樣。我使用esmodules
選項傳遞一個對象,而不是傳遞一個browserslist查詢。當設置爲true
時,@babel/preset-env
將使用較少的轉換,由於預設的目標是本機支持ES模塊和async
/ await
或其餘現代功能的瀏覽器。useBuiltIns
選項也刪除了,由於項目中使用的全部功能都不須要pollyfill。也就是說,若是你使用的時現代瀏覽器都不能很好地支持的前沿功能,你的應用程序可能須要一些polyfill。若是你的應用程序違反此設置,請進行適當地useBuiltIns
設置。正則表達式
webpack - 以及大多數其餘打包工具 - 將提供一種稱爲多編譯器模式的功能。此功能對於差別化服務相當重要。多編譯器模式容許你傳遞多個配置對象的數組以吐出多組包:shell
// webpack.config.js
modules.exports = [{
// Object config one
}, {
// Object config two
}];
複製代碼
這很重要,由於咱們能夠傳遞兩個使用相同入口點的獨立配置。咱們還能夠根據須要調整每一個配置中的規則。
不過,這提及來容易作起來難。webpack有時很是複雜,當你處理多種配置時,沒有比這更復雜的了。可是,這並不是不可能,因此讓咱們找出實現目標所需的條件吧。
由於你正在對相同的入口點進行單獨構建,因此你的配置將有不少共同之處。通用配置是管理這些類似性的便捷方式:
// webpack.config.js
const commonConfig = {
// `devMode` 是 process.env.NODE_ENV !== "production"的結果
mode: devMode ? "development" : "production",
entry: path.resolve(__dirname, "src", "index.js"),
plugins: [
// 在兩種配置中常見的插件
]
};
複製代碼
從這裏開始,你能夠編寫單獨的webpack配置並使用擴展語法將經常使用配置合併到每一個配置中:
// webpack.config.js
const legacyConfig = {
name: "client-legacy",
output: path.resolve(__dirname, "src", "index.js"),
module: {
rules: [
// loader...
]
},
// 使用擴展語法將經常使用配置合併到這個對象中.
...commonConfig
};
const modernConfig = {
name: "client-modern",
// 注意使用.mjs擴展名
output: path.resolve(__dirname, "src", "index.mjs"),
module: {
rules: [
// loader...
]
},
// 同上.
...commonConfig
};
module.exports = [legacyConfig, modernConfig];
複製代碼
這裏要說的是,若是將兩種配置之間的共同點與公共對象結合起來,能夠最大限度地減小必須編寫的配置行。從那裏,你只關注每一個目標之間的關鍵差別。
既然你知道如何管理每一個配置之間的共同點,那麼你須要知道如何管理不一樣的配置。當你編譯指向不一樣目標的公共入口點時,管理loader和插件會變得棘手。若是你處理的不只僅是JavaScript assets,尤爲如此。這是我但願會對你有所幫助的一些指導。
babel-loader
能夠說,你在任何webpack配置中看到的最多見的loader是babel-loader
。對於咱們想要實現的目標,你須要在你的新版和舊版配置對象中使用babel-loader
,儘管配置稍有不一樣。babel-loader
對於舊版瀏覽器目標,配置會像下面這樣:
// webpack.config.js
const legacyConfig = {
// ...
module: {
rules: [
{
test: /\.js$/i,
// 確保你的第三方庫打包到獨立的chunk中
// 不然這個排除模式也許會破壞你在客戶端的構建
exclude: /node_modules/i,
use: {
loader: "babel-loader",
options: {
envName: "legacy" // 指向babel.config.js中的env.legacy
}
}
},
// 其餘loader...
]
},
// ...
};
複製代碼
在現代瀏覽器目標中,惟一的區別是咱們將test
正則表達式的值改成/\.m?js$/i
來包含在npm包中的ES模塊文件擴展名(.mjs
)。咱們還須要將options.envName
的值修改成"modern"
。options.envName
指向在前面示例中babel.config.js
中包含的單獨配置。
// webpack.config.js
const modernConfig = {
// ...
module: {
rules: [
{
test: /\.m?js$/i,
exclude: /node_modules/i,
use: {
loader: "babel-loader",
options: {
envName: "modern" // 指向babel.config.js中的env.modern
}
}
},
// Other loaders...
]
},
// ...
};
複製代碼
根據你的項目,你可能有其餘loader或插件來處理JavaScript之外的assets類型。如何爲每一個目標瀏覽器處理它們取決於你的項目需求,但這裏有一些建議。
file-loader
處理導入非JavaScript資源。在現代瀏覽器的配置中,你能夠告訴file-loader
輸出文件,而在舊版瀏覽器的配置中,你能夠指定emitFile:false以防止文件被寫入磁盤兩次。這可能有助於提升速度。null-loader
對於使用多個配置來控制文件的加載和發送也多是有用的。image-webpack-loader
)來優化圖像。你可能須要在兩種配置中使用該loader,由於一個assets圖將包含對未優化圖像的引用,另外一個將包含對優化圖像的引用。因爲每一個構建的文件內容不一樣,所以文件哈希也會不一樣。結果是一組用戶將得到未優化的圖像assets,而其他用戶將得到優化的圖像assets。copy-webpack-plugin
將文件或整個目錄從src
複製到到dist
,你只須要它在一個配置中,而不是兩個。也就是說,在兩種配置中使用相同的插件不會致使問題,但可能會影響構建速度。pngquant-bin
),並在構建完成後在npm腳本中使用npx
來完成這項工做。這減小了個人webpack配置中的混亂,這是一個讓人樂於接受的改變。assets-webpack-plugin
爲兩個版本生成assets清單,那麼事情就會變得複雜。你須要建立單個實例以傳遞到每一個配置的插件數組並遵循此建議。在個人一個項目中,我在Node腳本中使用assets-webpack-plugin
將腳本引用注入到生成的HTML中(稍後會詳細介紹)。這些要點的要點是,你應該在構建之間保持assets引用的並行性,而且一般須要避免屢次將相同的assets寫入磁盤。可是,在可能已經很複雜的構建環境中也作了合理和方便的事情。
直到最近, uglify-js仍是webpack的默認uglifier。在4.26.0版本中terser成爲了默認設置。若是你使用的是版本4.26.0或更高版本,那麼對你來講是好消息 - 你已經完成全部設置,你無需再作任何額外的構建工做!
可是,若是你使用的是早期版本,則terser
不是 默認的uglifier,而是uglify-js
,你須要在你的新的配置中使用terser
。這是由於uglify-js
沒法理解ES5以外的JavaScript語法。它沒法識別像箭頭函數,async
/ await
,等等這些東西。
對於你的舊配置,你不須要作任何事情,由於它應該已經構建好了。可是,對於你新的配置,你須要爲你的項目npm install terser-webpack-plugin
。而後你須要添加terser-webpack-plugin
到optimization.minimizer
數組:
// webpack.config.js
const TerserWebpackPlugin = require("terser-webpack-plugin");
const modernConfig = {
// ...
optimization: {
minimizer: [
new TerserWebpackPlugin({
test: /\.m?js$/i, // 若是你要輸出.mjs文件,就須要這個
terserOptions: {
ecma: 6 // 也能夠是7或8
}
})
]
}
// ...
};
複製代碼
在咱們新的配置中,咱們輸出帶.mjs
擴展名的文件。爲了讓terser
可以識別和修改這些文件,咱們須要相應地修改test
正則表達式。咱們還須要將ecma
選項設置爲6
(儘管7
或8
也是有效值)。
你也許使用html-webpack-plugin
來爲你的應用程序外殼(app shell)標籤處理生成的HTML文件,這是有充分理由的。它是一個靈活的插件,能夠處理大量關於在HTML模板中插入<link>
和<script>
標記的繁忙工做。不幸的是,它的<script>
標籤插入方式不支持差別化服務。你能夠自行決定如何將這些腳本引用添加到HTML文件中。
幸運的是,只須要稍微動動腦就能夠解決這個問題。對於我使用差別化服務的項目,我使用assets-webpack-plugin
用來收集webpack生成的assets,以下所示:
// webpack.config.js
const AssetsWebpackPlugin = require("assets-webpack-plugin");
const assetsWebpackPluginInstance = new AssetsWebpackPlugin({
filename: "assets.json",
update: true,
fileTypes: [
"js",
"mjs"
]
});
複製代碼
從這裏開始,我將舊的和新的配置中的這個assets-webpack-plugin
實例添加到plugins
數組中。我已經使用如下配置插件實例選項來適配個人項目:
filename
指示應將assets JSON文件輸出到的位置。update
爲true
,它告訴插件爲新的和舊的配置重用相同的assets JSON文件。fileTypes
以確保在assets.json
中包含新的配置生成的.mjs
文件從這裏開始,它就是觸摸hacky的地方。爲了讓<script>
在個人HTML文件中引用想要的模式,我使用了一個在webpack完成後運行的npm腳本。此腳本讀取由assets-webpack-plugin
生成的assets.json
文件並在正確的標記中進行操做。
但願html-webpack-plugin
可以原生支持這一點,由於我我的不但願使用這種方法。你也許須要設計本身的臨時解決方案。
這篇文章你已經讀了一半了,我確信這個問題仍然存在:這種技術是否值得?個人回答是一個響亮的確定。我在個人網站上使用差別化服務,我認爲這些好處不言自明:
全部的JS assets | gzip(level 9) | Brotli(level 11) | |
---|---|---|---|
Legacy | 112.14 KB | 38.6 KB | 33.58 KB |
Modern | 34.23 KB | 12.94 KB | 12.12 KB |
對於個人網站來講,JavaScript大小減小了近70%。公平地說,隨着打包規模的擴大,我注意到差別化服務所帶來的節省明顯減小了。在個人工做中,我常常遇到超過300 KB的捆綁包,我已經看到接近10%的東西,但這仍然是一個顯着的減小!對我有利的大部分緣由是個人特定項目須要至關大量的polyfill用於傳統打包,而在不多甚至沒有polyfill或現代打包的變換中個人項目就能夠之間跳過這一步。
查看壓縮統計數據並說它可有可無也可能很誘人,但你必須始終牢記壓縮只會下降給定assets的傳輸時間。壓縮對解析/編譯/執行時間沒有影響。若是使用Brotli將100 KB JavaScript文件壓縮到30 KB,是的,用戶將很快收到它,但該文件仍然至關於100 KB的JavaScript。
在具備較低處理能力和內存的設備上,這是一個相當重要的區別。我常常在諾基亞2 Android手機上進行測試,差別化服務對加載性能的影響很明顯。如下是我在實施差別化服務以前訪問個人我的網站的Chrome設備中的性能跟蹤:
Chrome的DevTools中的網站性能跟蹤顯示在實施差別化服務以前的大量腳本活動。
下面是在差別化服務部署到位後它在同一設備上的表現:
Chrome的DevTools中的網站性能跟蹤顯示差別化服務實施後腳本活動量大大減小。
腳本活動減小約66%是可靠的。當網站更快地得到互動時,它們對每一個人來講都更加實用和愉快。
差別化服務是好東西。若是來自HTTPArchive的這種趨勢是任何指標,那麼生產中的大多數網站仍然會傳送大量的polyfilled和轉換後的遺留JS,這是不少用戶根本不須要的。若是咱們須要在舊版瀏覽器上支持用戶,咱們應該認真考慮這種左右開弓的方法來提供JavaScript。
若是不出意外,這應該會在將來觀察JavaScript的打包大小,以及如何保持對JavaScript工具在減小咱們發送給用戶的代碼方面的前瞻性態度上發揮做用。根據你的受衆,你甚至可能不須要提供兩個不一樣的捆綁包,但展現的配置可能會讓你瞭解如何發送比當前更少的代碼。
值得注意的是,JavaScript工具的狀態常常發生變化。感受很是像一個「快速移動和破壞東西」的空間。例如,webpack 5的alpha已經存在,而且須要進行大量更改。假設某些事情可能會壞掉並非不合理的。有趣的是,我仍然在個人工做中看到webpack 3上的項目。升級某些項目可能須要比其餘項目更多時間。這種技術 - 如文檔所述 - 在將來仍然有用。
若是你有興趣看到我如今使用此技術的網站,請查看此repo。但願你能從個人項目中學到儘量多的東西。