摘要:例如允許我們在打包時將腳本分塊利用瀏覽器緩存我們能夠有的放矢的加載資源。文章的內容大體分為兩個方面,一方面在思路制定模塊分離的策略,另一方面從技術上對方案進行落地。我之前提到測試之下是什么樣具體的場景并不重要。
前言隨著前端代碼需要處理的業務越來越繁重,我們不得不面臨的一個問題是前端的代碼體積也變得越來越龐大。這造成無論是在調式還是在上線時都需要花長時間等待編譯完成,并且用戶也不得不花額外的時間和帶寬下載更大體積的腳本文件。
然而仔細想想這完全是可以避免的:在開發時難道一行代碼的修改也要重新打包整個腳本?用戶只是粗略瀏覽頁面也需要將整個站點的腳本全部下載下來?所以趨勢必然是按需的、有策略性的將代碼拆分和提供給用戶。最近流行的微前端某種意義上來說也是遵循了這樣的原則(但也并不是完全基于這樣的原因)
幸運的是,我們目前已有的工具已經完全賦予我們實現以上需求的能力。例如 Webpack 允許我們在打包時將腳本分塊;利用瀏覽器緩存我們能夠有的放矢的加載資源。
在探尋最佳實踐的過程中,最讓我疑惑的不是我們能不能做,而是我們應該如何做:我們因該采取什么樣的特征拆分腳本?我們應該使用什么樣的緩存策略?使用懶加載和分塊是否有異曲同工之妙?拆分之后究竟能帶來多大的性能提升?最重要的是,在面多諸多的方案和工具以及不確定的因素時,我們應該如何開始?這篇文章就是對以上問題的梳理和回答。文章的內容大體分為兩個方面,一方面在思路制定模塊分離的策略,另一方面從技術上對方案進行落地。
本文的主要內容翻譯自 The 100% correct way to split your chunks with Webpack。 這篇文章循序漸進的引導開發者步步為營的對代碼進行拆分優化,所以它是作為本文的線索存在。同時在它的基礎上,我會對 Webpack 及其他的知識點做縱向擴展,對方案進行落地。
以下開始正文
現在讓我們把目光轉向 Alice 一遍又一遍下載的 main.js 文件
我之前提到過我們的站點里又兩個完全不同的部分:一個產品列表頁面和一個詳情頁面。每個頁面獨立的代碼提及大概是 25KB(共享 150KB 的代碼)
我們的“產品詳情”頁面目前不會進行更改,因為它非常的完美。所以如果我們把它劃分為獨立文件,大部分時候它都能夠從緩存中進行加載
你知道我們還有一個用于渲染 icon 用的 25KB 的幾乎不發生修改的 SVG 文件嗎?我們應該對它做些什么
我們手動的增加一些 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。我對那些還在使用古董機器的人做了調查,他們出奇一致的讓我放心他們根本不關心網站的加載速度
每一個 webpack 打包后的文件里會不會有多余的模板代碼?有的
但什么是“模板代碼”?
想象一下如果整個項目只有文件app.js,那么最終的輸出的打包文件也只是app.js的文件內容而已。
但是如果app.js文件內容是空的話(一行代碼都沒有),那么最終的打包文件也是空的嗎?
不是,Webpack 為了實現編譯之后的模塊化,它會將你的代碼進行一次封裝,這些用于封裝的代碼會占用一部分體積,是每個模塊都必須存在的,所以成為模板代碼
如果我有多個小文件的話是不是壓縮的效果就減弱了是的
事實確實是:
多文件 = 多 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??吹竭@種情況我會建議不把這些代碼分開,有以下幾個原因
它并不會讓加載變得更慢。記得你之前并行的加載這些文件,你可以試著記錄加載 300KB 和 307KB 的文件是否有變化
如果你延遲加載這部分代碼,用戶在點擊“付款”之后仍然需要等待文件的加載——你并不希望在這關鍵時刻給用戶帶來任何的阻力
代碼分離會導致程序代碼的更改,這需要將之前同步邏輯的地方改為異步邏輯。這并不復雜,但是對于改善用戶體驗這件事的性價比來說還是過于復雜了
這些就是我說的“這項令人振奮的技術或許不適合你”
讓我們看看兩個代碼分離的例子
回滾方案(Polyfills)我們從這個例子開始是因為它適用于大多數站點,并且是一個非常好的入門
我給我的站點使用了一堆酷炫的功,所以我使用了一個文件導入了我需要的所有回滾方案。它只需要八行代碼:
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,所以你需要把這部分的回滾代碼獨立出來)
非常簡單不是嗎?
有一些更棘手的場景
基于路由的動態加載(針對 React)回到 Alice 的例子,假設網站現在多了一個“管理”頁面,產品的賣家可以登陸并且管理他們售賣的產品
這個頁面有很多有用的功能,很多的圖表,需要安裝一個來自 npm 的表單類庫。因為我已經實現了打包代碼分離,目測至少已經節省了100KB 的大小文件
現在我設置了一份當用戶訪問呢/admin時渲染
但是我們不想這么做,我們希望在動態加載中加載管理頁面,比如import("./AdminPage.js"),這樣 Webpack 就知道需要動態加載它。
非??幔恍枰魏蔚呐渲?/p>
與直接引用AdminPage不同,當用戶訪問/admin時我使用另外一個組件用于實現如下功能:
核心思想非常簡單,當組件加載時(也就意味著用戶訪問/admin時),我們動態的加載./AdminPage.js然后在組件 state 中保存對它的引用
在渲染函數中,在等待
為了好玩我想自己實現它,但是在真實的世界里你只需要像React 關于代碼分離的文檔描述的那樣使用 react-loadable即可
以上就是所有內容了。以上我說的每一個觀點,還能說的更精簡嗎?
如果人們會不止一次的訪問你的站點,把你的代碼劃分為不同的小文件
如果你的站點有很大一部分用戶不會訪問到,動態的加載它們
謝謝閱讀,祝你有愉快的一天
完蛋了我忘記提 CSS 了
關于開發體驗以上我們都是在針對 production 對代碼進行分割。但事實上我們在開發過程中也會面臨同樣的問題:當代碼量增多時,打包的時間也在不斷增長。但是例如 node_modules 里的代碼千年不變,完全不需要被重新編譯。這部分我們也可以通過代碼分離的思想對代碼進行分離。比如 DLL 技術
通常我們說的 DLL 指的是 Windows 系統的下的動態鏈接庫文件,它的本意是將公共函數庫提取出來給大家公用以減少程序體積。我們的 DLL 也是借助了這種思想,將公共代碼分離出來。
使用 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 從哪找到預編譯的類庫代碼
引入 DLL 文件在正式打包應用腳本的 Webpack 配置中引入 DLL 即可:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require("./build/library/library.json")
})
]
不過美中不足的是,你仍然需要在你最終的頁面里引入 dll 文件
如果你的覺得手動配置 dll 仍然覺得繁瑣,那么可以嘗試使用 AutoDllPlugin
本文同時也發布在我的知乎專欄,歡迎大家關注
參考資料 Bundle VS ChunkWhat are module, chunk and bundle in webpack");
Concepts - Bundle vs Chunk
SurviveJS: Glossary
HashWhat is the purpose of webpack hash and chunkhash");
Hash vs chunkhash vs ContentHash
Adding Hashes to Filenames
SplitChunksPluginWebpack 4?—?Mysterious SplitChunks Plugin
Webpack (v4) Code Splitting using SplitChunksPlugin
Reduce JavaScript Payloads with Code Splitting
Webpack v4 chunk splitting deep dive
what reuseExistingChunk: true means, can give a sample"); DLL
How To Use The Dll Plugin to Speed Up Your Webpack Build
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/6875.html
摘要:而一個哈希字符串就是根據文件內容產生的簽名,每當文件內容發生更改時,哈希串也就發生了更改,文件名也就隨之更改。很顯然這不是我們需要的,如果文件內容發生了更改,的打包文件的哈希應該發生變化,但是不應該。前言 隨著前端代碼需要處理的業務越來越繁重,我們不得不面臨的一個問題是前端的代碼體積也變得越來越龐大。這造成無論是在調式還是在上線時都需要花長時間等待編譯完成,并且用戶也不得不花額外的時間和帶寬...
摘要:最近在研究的按需加載,好奇怪,之前好像并沒有看到的官文里面有這一部分,是我看差了嗎尬笑其實只需要看官文就可以了,里面有懶加載的講解,并且附帶了詳細內容的連接。所以很大程度上優化了頁面的初始加載速度。只是為了測試按需加載隨便寫的而已。 最近在研究vue的按需加載,好奇怪,之前好像并沒有看到vue的官文里面有這一部分,是我看差了嗎hahaha~尬笑~ 其實只需要看vue-router官文就...
摘要:當年的加載在沒有前端工程化之前,基本上是我們是代碼一把梭,把所需要的庫和自己的代碼堆砌在一起,然后自上往下的引用就可以了。而且對于前后端的技術要求較高,所以對于項目未必是最有效的方案。 當年的 js 加載 在沒有 前端工程化之前,基本上是我們是代碼一把梭,把所需要的庫和自己的代碼堆砌在一起,然后自上往下的引用就可以了。 那個時代我們沒有公用的cdn,也沒有什么特別好的方法來優化加載j...
摘要:在終端中使用可以自動創建這個文件輸入這個命令后,終端會問你一系列問題。百度后發現引入了模式,有三個狀態,開發模式生產模式無。 什么是webpack,為什么要使用webapck * 導語 之前一直忙著項目,沒時間整理自己的東西,最近剛好發現自己對webpack又如此陌生了,于是整理了一篇關于webpack初探的干貨,這里是一點簡單的webpack配置 為什么使用webpck 現今很多網頁...
閱讀 685·2023-04-25 22:50
閱讀 1525·2021-10-08 10:05
閱讀 983·2021-09-30 09:47
閱讀 1913·2021-09-28 09:35
閱讀 815·2021-09-26 09:55
閱讀 3405·2021-09-10 10:51
閱讀 3426·2021-09-02 15:15
閱讀 3290·2021-08-05 09:57