隨着前端代碼須要處理的業務愈來愈繁重,咱們不得不面臨的一個問題是前端的代碼體積也變得愈來愈龐大。這形成不管是在調式仍是在上線時都須要花長時間等待編譯完成,而且用戶也不得不花額外的時間和帶寬下載更大致積的腳本文件。javascript
然而仔細想一想這徹底是能夠避免的:在開發時難道一行代碼的修改也要從新打包整個腳本?用戶只是粗略瀏覽頁面也須要將整個站點的腳本所有下載下來?因此趨勢必然是按需的、有策略性的將代碼拆分和提供給用戶。最近流行的微前端某種意義上來講也是遵循了這樣的原則(但也並非徹底基於這樣的緣由)css
幸運的是,咱們目前已有的工具已經徹底賦予咱們實現以上需求的能力。例如 Webpack 容許咱們在打包時將腳本分塊;利用瀏覽器緩存咱們可以有的放矢的加載資源。html
在探尋最佳實踐的過程當中,最讓我疑惑的不是咱們能不能作,而是咱們應該如何作:咱們因該採起什麼樣的特徵拆分腳本?咱們應該使用什麼樣的緩存策略?使用懶加載和分塊是否有殊途同歸之妙?拆分以後究竟能帶來多大的性能提高?最重要的是,在面多諸多的方案和工具以及不肯定的因素時,咱們應該如何開始?這篇文章就是對以上問題的梳理和回答。文章的內容大致分爲兩個方面,一方面在思路制定模塊分離的策略,另外一方面從技術上對方案進行落地。前端
本文的主要內容翻譯自 The 100% correct way to split your chunks with Webpack。 這篇文章按部就班的引導開發者步步爲營的對代碼進行拆分優化,因此它是做爲本文的線索存在。同時在它的基礎上,我會對 Webpack 及其餘的知識點作縱向擴展,對方案進行落地。java
如下開始正文node
如今讓咱們把目光轉向 Alice 一遍又一遍下載的 main.js
文件react
我以前提到過咱們的站點裏又兩個徹底不一樣的部分:一個產品列表頁面和一個詳情頁面。每一個頁面獨立的代碼說起大概是 25KB(共享 150KB 的代碼)jquery
咱們的「產品詳情」頁面目前不會進行更改,由於它很是的完美。因此若是咱們把它劃分爲獨立文件,大部分時候它都可以從緩存中進行加載webpack
你知道咱們還有一個用於渲染 icon 用的 25KB 的幾乎不發生修改的 SVG 文件嗎?咱們應該對它作些什麼git
咱們手動的增長一些 entry 入口,告訴 Webpack 給它們都建立獨立的文件:
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/index.js'),
ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
},
plugins: [
new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
],
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
};
複製代碼
而且 Webpack 自動爲它們之間的共享代碼也建立了獨立的文件,也就是說ProductList
和ProductPage
不會擁有重複的代碼
這回 Alice 在大多數週裏都會節省下 50KB 的下載量
只有 1.815MB 了
咱們已經爲 Alice 節省了 56% 的下載量,而且會持續下去(在咱們的理論場景中)
全部這些都是經過修改 Webapck 配置實現的——咱們尚未修改任何一行應用程序的代碼。
我以前提到測試之下是什麼樣具體的場景並不重要。由於不管你碰見的是什麼場景,結論始終是一致的:把你的代碼劃分爲更多更有意義的小文件,用戶須要下載的代碼也就越少
很快咱們就將談到「代碼分離」——另外一種分割文件的方式——可是首先我想首先解決你如今正在考慮的問題
答案很是明確是否認的
在 HTTP/1.1 的狀況下確實會如此,可是在 HTTP/2 中不會
儘管如此,這篇來自 2016 年的文章和來自於Khan Academy 2015 年的文章都得出結論說即便有 HTTP/2 下載太多文件的話仍然會致使變慢。可是在這兩篇文章裏「太多」意味着上百個文件。因此只要記住若是你有上百個文件,你或許達到了並行的上限
若是你在好奇如何在 Windows 10 的 IE11 上支持 HTTP/2。我對那些還在使用古董機器的人作了調查,他們出奇一致的讓我放心他們根本不關心網站的加載速度
有的
但什麼是「模板代碼」?
想象一下若是整個項目只有文件app.js
,那麼最終的輸出的打包文件也只是app.js
的文件內容而已。
可是若是app.js
文件內容是空的話(一行代碼都沒有),那麼最終的打包文件也是空的嗎?
不是,Webpack 爲了實現編譯以後的模塊化,它會將你的代碼進行一次封裝,這些用於封裝的代碼會佔用一部分體積,是每一個模塊都必須存在的,因此成爲模板代碼
是的
事實確實是:
讓咱們把其中的損耗的都明確下來
我剛剛作了一個測試,一個 190 KB 的站點文件被劃分爲了19個文件,發送給瀏覽器的字節數大概多了 2%
因此……首次訪問的文件說起增長了 2% 可是直到世界末日其餘的每次訪問文件體積都減少了 60%
因此損耗的正確數字是:一點都不。
當我在測試 1 個文件對比 19 個文件狀況時,我想我應該賦予測試一些不一樣的網絡環境,包括 HTTP/1.1
下面這張表格給予了「文件越多越好」的有力支持
在 3G 和 4G 的狀況下當有19個文件時加載時間減小了 30%
但真的是這樣嗎?
這份數據看上去「噪點」不少,舉個例子,在 4G 場景下第二次運行時,網站加載花費了 646ms,可是以後的第二輪運行則花費了 1116ms——時間增長了73% 。因此宣稱 HTTP/2 快了 30% 有一些心虛
我建立這張表格是爲了試圖量化 HTTP/2 究竟能帶來多大的差別,可是我惟一能說的是「並無太大的區別」
真正使人驚喜的是最後兩行,舊版本的 Windows 和 HTTP/1.1 我本覺得會慢很是多。我猜我須要更慢的網絡環境用於進行驗證
故事時間!我從微軟網站下載了一個 Windows 7 的虛擬機來測試這些東西
我想把默認的 IE8 升級至 IE9
因此我前往微軟下載 IE9 的頁面而後發現:
最後提一句 HTTP/2,你知道它已經集成進 Node 中了嗎?若是你想嘗試,我用100行寫了一段 HTTP/2 服務,可以爲你的測試帶來緩存上的幫助
以上就是我想說的關於打包分離的一切。我想這個實踐惟一的壞處是須要說服人們加載如此多的小文件是沒有問題的
這個特殊的實踐只對某些站點有效
我樂意重申一下我發明的 20/20 理論:若是站點的某些部分只有 20% 用戶會訪問,而且這部分的腳本量大於你整個站點的 20% 的話,你就應該考慮按需加載代碼了
你能夠對數值進行調整來適配更復雜的場景。重點是保持平衡,須要決策將對站點無心義的代碼分離出來
假設你有擁有一個購物網站,你在糾結是否應該把「結帳」功能的代碼分離出來,由於只有 30% 的用戶會走到那一步
首先是要讓賣的更好
其次計算出「結帳」功能的獨立代碼有多少。由於在作「代碼分離」以前你經常作「打包文件分離」,你或許已經知道了這部分代碼量有多少
(它可能比你想象的還要小,因此計算以後你可能得到驚喜。若是你有一個 React 站點,你的 store,reducer,routing,actions 可能會被整個網站共享,獨立的部分可能大部分是組件和幫助類庫)
假設你注意到結算頁面獨立代碼一共只有 7KB,其餘部分的代碼 300KB。看到這種狀況我會建議不把這些代碼分開,有如下幾個緣由
這些就是我說的「這項使人振奮的技術或許不適合你」
讓咱們看看兩個代碼分離的例子
咱們從這個例子開始是由於它適用於大多數站點,而且是一個很是好的入門
我給個人站點使用了一堆酷炫的功,因此我使用了一個文件導入了我須要的全部回滾方案。它只須要八行代碼:
require('whatwg-fetch');
require('intl');
require('url-polyfill');
require('core-js/web/dom-collections');
require('core-js/es6/map');
require('core-js/es6/string');
require('core-js/es6/array');
require('core-js/es6/object');
複製代碼
我在個人入口文件index.js
頂部引入了這個文件
import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';
const render = () => {
ReactDOM.render(<App />, document.getElementById('root')); } render(); // yes I am pointless, for now 複製代碼
在 Webpack 配置關於打包分離的小節配置中,個人回滾代碼會自動被分爲四個不一樣的文件由於有四個 npm 包。它們一共大小 25KB 左右,而且 90% 的瀏覽器都不須要它們,因此它們值得動態的進行加載。
在 Webpack 4 以及 import()
語法(不要和import
語法混淆了)的支持下,有條件的加載回滾代碼變得很是簡單了
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';
const render = () => {
ReactDOM.render(<App />, document.getElementById('root')); } if ( 'fetch' in window && 'Intl' in window && 'URL' in window && 'Map' in window && 'forEach' in NodeList.prototype && 'startsWith' in String.prototype && 'endsWith' in String.prototype && 'includes' in String.prototype && 'includes' in Array.prototype && 'assign' in Object && 'entries' in Object && 'keys' in Object ) { render(); } else { import('./polyfills').then(render); } 複製代碼
如今是否是更有意義了?若是瀏覽器支持全部的新特性,那麼渲染頁面。不然加載回滾代碼渲染頁面。當代碼在運行在瀏覽器中時,Webpack 的運行時會負責這四個包的加載,而且當它們被下載而且解析完畢時,render()
函數纔會被調用,而且其它工做繼續運行
(順便說一聲,若是須要使用import()
的話,你須要 Babel 的 dynamic-import 插件 。而且如 Webpack 文檔解釋的,import()
使用 Promises,因此你須要把這部分的回滾代碼獨立出來)
很是簡單不是嗎?
有一些更棘手的場景
回到 Alice 的例子,假設網站如今多了一個「管理」頁面,產品的賣家能夠登錄而且管理他們售賣的產品
這個頁面有不少有用的功能,不少的圖表,須要安裝一個來自 npm 的表單類庫。由於我已經實現了打包代碼分離,目測至少已經節省了100KB 的大小文件
如今我設置了一份當用戶訪問呢/admin
時渲染<AdminPage>
的路由。當 Webpack 把一切都打包完畢以後,它會去查找import AdminPage from './AdminPage.js'
,而且說「嘿,我須要把它包含到初始化的加載文件中」
可是咱們不想這麼作,咱們但願在動態加載中加載管理頁面,好比import('./AdminPage.js')
,這樣 Webpack 就知道須要動態加載它。
很是酷,不須要任何的配置
與直接引用AdminPage
不一樣,當用戶訪問/admin
時我使用另一個組件用於實現以下功能:
核心思想很是簡單,當組件加載時(也就意味着用戶訪問/admin
時),咱們動態的加載./AdminPage.js
而後在組件 state 中保存對它的引用
在渲染函數中,在等待<AdminPage
>加載的過程當中咱們簡單的渲染出<div>Loading...</div>
,一旦加載成功則渲染出<AdminPage>
爲了好玩我想本身實現它,可是在真實的世界裏你只須要像React 關於代碼分離的文檔描述的那樣使用 react-loadable
便可
以上就是全部內容了。以上我說的每個觀點,還能說的更精簡嗎?
謝謝閱讀,祝你有愉快的一天
完蛋了我忘記提 CSS 了
以上咱們都是在針對 production 對代碼進行分割。但事實上咱們在開發過程當中也會面臨一樣的問題:當代碼量增多時,打包的時間也在不斷增加。可是例如 node_modules 裏的代碼千年不變,徹底不須要被從新編譯。這部分咱們也能夠經過代碼分離的思想對代碼進行分離。好比 DLL 技術
一般咱們說的 DLL 指的是 Windows 系統的下的動態連接庫文件,它的本意是將公共函數庫提取出來給你們公用以減小程序體積。咱們的 DLL 也是藉助了這種思想,將公共代碼分離出來。
使用 DLL 簡單來講分爲兩步:
咱們將咱們須要分離的文件到打包爲 DLL 文件,以分離 node_modules 類庫爲例,關鍵配置以下。注意這段配置僅僅是用於分離 dll 文件,並不是打包應用腳本
module.exports = {
entry: {
library: [
'react',
'redux',
'jquery',
'd3',
'highcharts',
'bootstrap',
'angular'
]
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, './build/library'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: './build/library/[name].json'
})
]
};
複製代碼
關鍵在於使用 DLLPlugin 輸出的 json 文件,用於告訴 webpack 從哪找到預編譯的類庫代碼
在正式打包應用腳本的 Webpack 配置中引入 DLL 便可:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./build/library/library.json')
})
]
複製代碼
不過美中不足的是,你仍然須要在你最終的頁面裏引入 dll 文件
若是你的以爲手動配置 dll 仍然以爲繁瑣,那麼能夠嘗試使用 AutoDllPlugin
本文同時也發佈在個人知乎專欄,歡迎你們關注