(六)构建优化(揭开webpack性能优化的内幕)
構建優化
- webpack的優化配置【了解這些優化配置才敢說會用webpack】
- Tree-shaking
- JS壓縮
- 作用域提升
- Babel的優化配置
- webpack的依賴優化
- noParse(不解析)
- DllPlugin
- 基于webpack的代碼拆分【讓網站按需加載】
- 代碼拆分做什么
- webpack代碼拆分方法
- 手把手教你做webpack的資源壓縮
- Minification
- 基于webpack的持久化緩存
- 持久化緩存解決方案
- 基于webpack的應用大小監測與分析【webpack性能分析的法寶】
- React按需加載的實現方式
- 總結代碼拆分和不同技術的適用場景
webpack的優化配置【了解這些優化配置才敢說會用webpack】
之前我們講了不少的優化方法,如果都是通過手工去做會非常麻煩,如果可以利用webpack這樣的構建工具幫我們自動化的完成這些任務,可以大大提高我們的開發效率
可以配置很多plugins和loader幫我們做很多工作,但我們很難記住所有的plugins和loader,從webpack4開始引入了mode模式,可以配置開發和生產模式,就可以使用一些已經默認好的插件來幫我們達到一些想做的效果,不用再為這些配置發愁,這也是計算機常用的模式,叫做CONVENTION OVER CONFIGURATION(約定大于配置),它給我們做了兩個約定好的模式,我們直接使用就好了,如果這個模式里有些細微的東西我們覺得不太合適,做一些調整,可以重載里面的配置,不用大規模的自己重新進行配置,關于兩種模式具體有哪些默認插件可以去官網進行了解
npm run dev開發模式
npm run start生產模式
Tree-shaking
-
上下文未用到的代碼(dead code)
-
基于ES6 import export
做Tree-shaking有一個基礎,無論是你自己的還是引入第三方的,必須是模塊化的,要基于ES6 import export導入導出的形式才可以,生產模式默認會啟用Tree-shaking的功能,主要是基于它需要依賴的插件TerserPlugin,用來做壓縮,實現的簡單原理:會根據入口文件,相當于一棵樹的根節點,起點,從index.jsx開始,去看它引用了哪些東西,進一步分析所有引入的包或模塊里又引用了哪些模塊或者其他的一些包,不斷分支分支去分析之后,會把所有需要的東西保留下來,把那些雖然我們引用了包里有的東西但我們沒有用到的全部給它搖下去,所以最終我們得到的bundle只包含了我們代碼運行時需要的東西 -
package.json中配置sideEffects
Tree-shaking雖然很好,但也有它的局限性,它的實現是基于一定的規則,需要基于es6的模塊化導入導出語法,在js里,我們可能會涉及到修改全局作用域,這里全局作用域對于前端或瀏覽器而言,就是window對象,可能在全局上添加了方法或者修改了屬性,這個時候是體現不出來的,如果它把你這個shake掉了,代碼就會出問題,所以它給我們留了后門,就是我們可以指定并告訴webpack哪些東西是有副作用的,不能在Tree-shaking中去掉
-
注意Babel默認配置的影響
preset把常用的babel插件做了一個集合,我們調下這個集合就可以用這些插件,轉碼的時候會把es6模塊化的語法轉成其他模塊化語法,我們希望保留es6模塊化語法,所以要加上modules: false的配置,這樣Tree-shaking才能起到作用
JS壓縮
- Webpack4后引入uglifyjs-webpack-plugin
- 支持ES6替換為terser-webpack-plugin
terser無論從效率還是效果上都比uglifyjs好,所以terser后面作為生產模式下默認的壓縮插件,而且可以支持es6語法 - 減小js文件體積
作用域提升
-
代碼體積減小
減少了調用關系邏輯上的代碼,把一些函數進行了合并 -
提高執行效率
要進行引用的話,肯定要花時間進行查找,引用進來再進行調用 -
同樣注意Bable的modules的配置
加上modules: false的配置,因為所有這些也要基于es6的import、export的語法
沒有啟用作用域提升的話,會把這兩個模塊打成單獨的模塊,當其中一個依賴到另一個時,會把依賴到的模塊require進來,再通過require進來的模塊進行調用
如果啟用了作用域提升,會做一個合并,會進行分析,發現有這種依賴調用時,試圖把依賴合并到調用里,最終變得更加精簡,只有一個函數,當我們使用webpack生產模式時,會自動幫我們做這個作用域提升
Babel的優化配置
-
在需要的地方引入polyfill
polyfill是兼容舊瀏覽器去進行新的功能或者新的規范的一些實現
給瀏覽器不支持的語法打補丁,比如promise、include等
需要安裝@babel/polyfill,安裝了這個之后我們就可以兼容這些東西,但這個東西有些過大,把所有涉及到的東西都引入進來了,但我們用到的可能只是其中很小的部分,配置“useBuiltIns”: "usage"就可以達到我們的效果
-
輔助函數的按需引入
聲明了一個class,babel轉碼后是如下圖的形式,使用了_classCallCheck這樣的輔助函數,每當我們聲明一個新的類時,都會生成這個輔助函數,但這個輔助函數是可以進行復用的,復用可以減少不少的代碼,輔助函數的按需引入是對輔助函數的復用,只要把@babel/plugin-transform-runtime插件配置上就可以,剩下工作交給babel做
-
根據目標瀏覽器按需轉換代碼
怎么通過babel設置目標瀏覽器
要對市場份額超過百分之0.25的所有瀏覽器都要進行支持,babel就要根據你的配置去決定最后轉碼要轉成什么樣,要轉成支持你的要求的,如果支持得越少,它要做的轉碼工作或者轉出來的代碼體積就越小,但對用戶的支持和體驗來說是不好的,所以這通常要根據我們的實際情況進行設置
有哪些可以放在browers里進行設置,babel集成的是browserslist插件來進行刷選
webpack的依賴優化
使得webpack打包本身的這個過程可以得到一個提速
noParse(不解析)
- 提高構建速度
- 直接通知webpack忽略較大的庫
那哪些庫可能會被考慮在范圍里呢?通常是一些我們引用的第三方的一些類庫,或者是一些工具類,他本身到是一些比較大的庫,再加上呢它使用的是比較傳統的方式,也就是說,沒有模塊化的方式,去進行編寫的,那么他本身也不會有什么外部的依賴,所以這樣的庫他本身比較獨立,又比較大,那我們干脆就不對它去進行解析 - 被忽略的庫不能有import,require,define的引入方式
那反過來說呢,就是我們被忽略的這些庫啊,要有一個特點,就是它不能是模塊化的方式去編寫的,可以通過去識別一些關鍵字,像import,require,define,我們就可以知道這個庫是不是這種方式
DllPlugin
把我們經常使用的一些重復的庫可以把它提取出來變成一種引用的方式,這樣的話我們就不用每一次都對這些庫進行一個重新的構建,可以大大的加速我們這個構建的過程
- 避免打包時對不變的庫重復構建
哪些是不變化庫?比如react和react-dom其實從我們開始做這個工程到我們最后上線可能都不會再變了,每一次構建都在對這兩個進行重復構建,如果我們可以把它提取出來變成一個類似動態鏈接庫的引用,不用再重復構建,只需要去引用之前構建過的固定內容就可以了 - 提高構建速度
- 應用場景
生產環境應用的可能性比較小,生產不會經常打包,生產打包慢點也不會介意,反而開發環境用得更多
package.json
webpack.dll.config.js
const path = require("path"); const webpack = require("webpack"); module.exports = {mode: "production",entry: {react: ["react", "react-dom"],},output: {filename: "[name].dll.js",path: path.resolve(__dirname, "dll"),library: "[name]"},plugins: [new webpack.DllPlugin({name: "[name]",path: path.resolve(__dirname, "dll/[name].manifest.json")})] };webpack.config.js
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');plugins: [/* 動態鏈接庫引用 */new DllReferencePlugin({manifest: require(`${__dirname}/dll/react.manifest.json`)}) }動態鏈接庫優化前打包11s,優化后打包3s
基于webpack的代碼拆分【讓網站按需加載】
代碼拆分做什么
- 把單個bundle文件拆分成若干小bundles/chunks
- 縮短首屏加載時間
webpack代碼拆分方法
- 手工定義入口
- splitChunks提取公有代碼,拆分業務代碼與第三方庫
第一個目目就是我們需要把我們的這個代碼中重復被使用到的這樣的一些東西去提取出來,第二就是我們要把我們的這個業務邏輯和我們使用的第三方依賴的進行一個拆分,這是一個很好的一個最佳實踐,就是我們要把我們自己的業務和這個業務的依賴去進行一個拆分,因為我們知道我們這個業務可能會經常變,但是第三個庫的這些東西可能會經常不變,從緩存這個角度考慮,我們也應該進行一個拆分
- 動態加載
手把手教你做webpack的資源壓縮
Minification
-
Terser壓縮js
當webpack啟用生產模式時,默認會啟動很多的插件,其中包括Terser
terser插件文檔
-
mini-css-extract-plugin壓縮css
安裝 “mini-css-extract-plugin”: “^0.9.0”,//樣式對象提取到單獨文件,css與js進行拆分,拆成兩個不同文件,加載時彼此不會影響
“optimize-css-assets-webpack-plugin”: “^5.0.3”,// CSS壓縮優化
- HtmlWebpackPlugin-minify壓縮HTML
去除空白、刪除注釋、刪除冗余屬性、刪除script屬性、刪除stylelink屬性、使用html5描述方式
基于webpack的持久化緩存
利用緩存可以幫我們提高用戶在再次訪問網站時的體驗,加快網頁加載速度,如何管理好這些緩存,要保證html,css,js都是最新的代碼
更新部署過程中,這些資源的更新是有個時間間隔的,有先有后,假如這時用戶進行訪問,就很容易出現問題,可能拿到最新的html,但是相關資源未拿到,這時瀏覽器就會使用之前緩存的js、css,新的代碼和舊的代碼一起,會出現問題,該怎么管理好緩存?
持久化緩存解決方案
- 每個打包的資源文件有唯一的hash值
- 修改后只有受影響的文件hash變化
- 充分利用瀏覽器緩存
在所有靜態資源后面加一個hash值,hash值可以通過文件內容計算出來,hash有個特點是離散唯一的值,如果我們文件的內容不變,計算出來的值也不變,而且呢,是唯一的,一旦我們這個文件的內容被修改過了,也就是我們進行了更新,我們要進行再次部署的時候,這個文件所生成的對應的hash值,也會變化成一個新的值,而且還是唯一的,這樣的話,我們就可以做一個增量式的更新,避免我們剛才說的那個問題,即使是在你部署的這個過程中,有這種更新的,這種時間間隔我們也不會出現用戶某些文件使用的是新文件,某些使用的是舊的文件,這樣就可以充分的利用我們的這個瀏覽器緩存,還保證用戶這種體驗的情況下,能夠進行一個平穩的更新過度
在我們命名的時候,利用他給我們的這樣的一個預置的一個變量,把他它在我們這個文件名里
output: {path: `${__dirname}/build`,filename: '[name].[hash].bundle.js',// 沒有進行按需加載的文件的命名規則,整個應用唯一的hashchunkFilename: '[name].[chunkhash:8].bundle.js'// 進行按需加載被拆出來的代碼打成的包所要進行的命名規則,每個動態組件可能對應若干不同的chunk或者代碼段,每個代碼端都有自己唯一的hash值,這邊用8位的hash值作為命名},plugins: [new MiniCssExtractPlugin({filename: '[name].[hash].css',chunkFilename: '[id].[chunkhash:8].css',}),]打出來的包js和css的hash一致
修改成contenthash后,打出來的包css和js不一致
如果只修改了js沒有修改css,不會因為css是從js中提取出來的,而他們使用同個bundle值導致文件名也發生變化,因為css沒發生實質變化,可以保持原有hash值,這樣更新部署時不需要被更新,還可以利用之前的緩存
基于webpack的應用大小監測與分析【webpack性能分析的法寶】
主要是三個工具,那其中前兩個工具主要是對我們的這個代碼去進行靜態分析,然后了解bundle里每個模塊的體積是什么樣的?然后最后一個插件呢,是關注我們速度這塊
- Stats分析與與可視化圖
- webpack-bundle-analyzer進行體積分析
- speed-measure-webpack-plugin速度分析
webpack-chart是webpack官網推薦的一個性能分析工具
優點:在線
使用方法:通過webpack --profile --json > stats.json的命令把我們這個分析的數據導出來,這個數據還是來自于webpack,webpack去進行這個打包的時候,實際上可以給我們去生成這樣的一個性能分析的數據文件,它是一個json文件,我們自己看會很麻煩,通過這種可視化的方式讓大家更容易去把它讀懂,得到這個文件后在這里進行上傳,右側的極坐標圖會根據分析的數據進行展示,這個圖是自內向外讀的,里層的代表的是整個bundle的大小,這個圖可以比較清晰的展示每一部分是由什么組成的,可以一層一層的去剖析
上面的工具只能看個大概,還是不夠細,想進一步可以用bundle-analyzer這樣的工具實現,這里用source-map-explorer,通過這個工具可以進一步進行分析,對build里所有的js進行分析,這個工具有個特點,首先分析不是基于最后我們的bundle文件,而是基于sourcemap,所以我們需要生成sourcemap
先npm run build再npm run analyze,她會很快的幫我們去進行分析,然后打開一個網頁給我們展示這個測試的報告,這個報告左上角有一個下拉菜單,我們通過這個去調整,去看具體的某一個bundle,或者這個第一個叫做這個combined就是我們所有的bundle的一個整體的分析,通過這個很容易了解到我們的每一個包里是什么東西?每塊他到底占比怎么樣?是不是需要去進行優化?
另外看一下,官方推薦的bundle-analyzer,如果我們使用它去進行這樣的類似這個分析的話,也可以得到類似這樣的一個可視化的圖,他也是通過這種矩形包含的關系去給我們展示這個占比的一個情況,而且他這個這個不同的這個顏色來進行區分,但是我覺得這個圖可能跟我們剛才source-map-explorer相比,最大的一個缺點就是他沒有把每一部分的這個體積和占比情況直接給我們標出來,看這個圖還是有點類似剛剛的在線工具,只能看個大概,但是你去點擊具體的部分時,會列出一些具體的內容,仍然沒有占比信息,所以推薦使用source-map-explorer
還有個速度分析
運行npm run build,看如下構建日志,可以看到所有plugins和所有loaders的使用效率情況,會列出具體的工作時總的耗時
React按需加載的實現方式
- React router基于webpack動態引入
- 使用Reloadable高級組件
如何使用Reloadable高級組件來做基于路由的按需加載
// App.jsx import loadable from '@loadable/component'; // 使用React-Loadable動態加載組件 const LoadableAbout = loadable(() => import('./About.jsx'), {fallback: '<div>loading...</div>' }); class App extends React.Component {constructor(props) {super(props);}render() {return (<Router><Switch><MuiThemeProvider theme={theme}><div><Header/><Route exact path="/" component={Home}/><Route path="/about" component={LoadableAbout}/></div></MuiThemeProvider></Switch></Router>);} }總結代碼拆分和不同技術的適用場景
代碼拆解最初是為了解決我們這一個過大請求的問題,我們知道我們之前是把所有的資源都打成一個包,那么我們通過一個請求,把整個剝去加載到我們的這個首頁,那么這個時候他在網絡上的這個開銷雖然相對的少了一些,因為只有一個請求,但是因為整個包體積比較大,所以他下載耗時非常的長,所以我們就做了一個事情,就是我們這個較大的包進行一個拆解,把它拆成了若干較小的包,這每個小的包只有當其中被用到時才會被加載,這就是按需加載,但這也有個問題,就是我們拆解要拆到什么力度,也就是這模塊的定義,假如我們是定義到組件的水平,那所有組件都被拆成一個獨立模塊的話,我們會有很多很多bundle或者chunk,每當用到一個組件時就需要進行按需加載,會帶來什么問題?我們想要獲得這個頁面上所有的資源,有若干個組件,就需要發起若干個請求,每個請求都有自己的網絡開銷,這些網絡開銷累積起來可能比之前一個請求的開銷還大,所以拆解力度要控制好,通常最合理的方式是按照路由進行按需加載,而當我們頁面上的一些組件在不同路由頁面會被進行復用時,才把組件單獨進行拆解
總結
以上是生活随笔為你收集整理的(六)构建优化(揭开webpack性能优化的内幕)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux中tcpdump命令实例详解
- 下一篇: (四)代码优化 (快来看看怎样写出真正高