摘要:但是,隨者工程開發的復雜程度和代碼規模不斷地增加,暴露出來的各種性能問題也愈發明顯,極大的影響著開發過程中的體驗。對應的資源也可以直接由頁面外鏈載入,有效地減小了資源包的體積。
背景
如今前端工程化的概念早已經深入人心,選擇一款合適的編譯和資源管理工具已經成為了所有前端工程中的標配,而在諸多的構建工具中,webpack以其豐富的功能和靈活的配置而深受業內吹捧,逐步取代了grunt和gulp成為大多數前端工程實踐中的首選,React,Vue,Angular等諸多知名項目也都相繼選用其作為官方構建工具,極受業內追捧。但是,隨者工程開發的復雜程度和代碼規模不斷地增加,webpack暴露出來的各種性能問題也愈發明顯,極大的影響著開發過程中的體驗。
問題歸納歷經了多個web項目的實戰檢驗,我們對webapck在構建中逐步暴露出來的性能問題歸納主要有如下幾個方面:
代碼全量構建速度過慢,即使是很小的改動,也要等待長時間才能查看到更新與編譯后的結果(引入HMR熱更新后有明顯改進);
隨著項目業務的復雜度增加,工程模塊的體積也會急劇增大,構建后的模塊通常要以M為單位計算;
多個項目之間共用基礎資源存在重復打包,基礎庫代碼復用率不高;
node的單進程實現在耗cpu計算型loader中表現不佳;
針對以上的問題,我們來看看怎樣利用webpack現有的一些機制和第三方擴展插件來逐個擊破。
慢在何處作為工程師,我們一直鼓勵要理性思考,用數據和事實說話,“我覺得很慢”,“太卡了”,“太大了”之類的表述難免顯得太籠統和太抽象,那么我們不妨從如下幾個方面來著手進行分析:
從項目結構著手,代碼組織是否合理,依賴使用是否合理;
從webpack自身提供的優化手段著手,看看哪些api未做優化配置;
從webpack自身的不足著手,做有針對性的擴展優化,進一步提升效率;
在這里我們推薦使用一個wepback的可視化資源分析工具:webpack-bundle-analyzer,在webpack構建的時候會自動幫你計算出各個模塊在你的項目工程中的依賴與分布情況,方便做更精確的資源依賴和引用的分析。
從上圖中我們不難發現大多數的工程項目中,依賴庫的體積永遠是大頭,通常體積可以占據整個工程項目的7-9成,而且在每次開發過程中也會重新讀取和編譯對應的依賴資源,這其實是很大的的資源開銷浪費,而且對編譯結果影響微乎其微,畢竟在實際業務開發中,我們很少會去主動修改第三方庫中的源碼,改進方案如下:
方案一、合理配置 CommonsChunkPluginwebpack的資源入口通常是以entry為單元進行編譯提取,那么當多entry共存的時候,CommonsChunkPlugin的作用就會發揮出來,對所有依賴的chunk進行公共部分的提取,但是在這里可能很多人會誤認為抽取公共部分指的是能抽取某個代碼片段,其實并非如此,它是以module為單位進行提取。
假設我們的頁面中存在entry1,entry2,entry3三個入口,這些入口中可能都會引用如utils,loadash,fetch等這些通用模塊,那么就可以考慮對這部分的共用部分機提取。通常提取方式有如下四種實現:
1、傳入字符串參數,由chunkplugin自動計算提取
new webpack.optimize.CommonsChunkPlugin("common.js")
這種做法默認會把所有入口節點的公共代碼提取出來, 生成一個common.js
2、有選擇的提取公共代碼
new webpack.optimize.CommonsChunkPlugin("common.js",["entry1","entry2"]);
只提取entry1節點和entry2中的共用部分模塊, 生成一個common.js
3、將entry下所有的模塊的公共部分(可指定引用次數)提取到一個通用的chunk中
new webpack.optimize.CommonsChunkPlugin({ name: "vendors", minChunks: function (module, count) { return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, "../node_modules") ) === 0 ) } });
提取所有node_modules中的模塊至vendors中,也可以指定minChunks中的最小引用數;
4、抽取enry中的一些lib抽取到vendors中
entry = { vendors: ["fetch", "loadash"] }; new webpack.optimize.CommonsChunkPlugin({ name: "vendors", minChunks: Infinity });
添加一個entry名叫為vendors,并把vendors設置為所需要的資源庫,CommonsChunk會自動提取指定庫至vendors中。
方案二、通過 externals 配置來提取常用庫在實際項目開發過程中,我們并不需要實時調試各種庫的源碼,這時候就可以考慮使用external選項了。
簡單來說external就是把我們的依賴資源聲明為一個外部依賴,然后通過script外鏈腳本引入。這也是我們早期頁面開發中資源引入的一種翻版,只是通過配置后可以告知webapck遇到此類變量名時就可以不用解析和編譯至模塊的內部文件中,而改用從外部變量中讀取,這樣能極大的提升編譯速度,同時也能更好的利用CDN來實現緩存。
external的配置相對比較簡單,只需要完成如下三步:
1、在頁面中加入需要引入的lib地址,如下:
2、在webapck.config.js中加入external配置項:
module.export = { externals: { "react-router": { amd: "react-router", root: "ReactRouter", commonjs: "react-router", commonjs2: "react-router" }, react: { amd: "react", root: "React", commonjs: "react", commonjs2: "react" }, "react-dom": { amd: "react-dom", root: "ReactDOM", commonjs: "react-dom", commonjs2: "react-dom" } } }
這里要提到的一個細節是:此類文件在配置前,構建這些資源包時需要采用amd/commonjs/cmd相關的模塊化進行兼容封裝,即打包好的庫已經是umd模式包裝過的,如在node_modules/react-router中我們可以看到umd/ReactRouter.js之類的文件,只有這樣webpack中的require和import * from "xxxx"才能正確讀到該類包的引用,在這類js的頭部一般也能看到如下字樣:
if (typeof exports === "object" && typeof module === "object") { module.exports = factory(require("react")); } else if (typeof define === "function" && define.amd) { define(["react"], factory); } else if (typeof exports === "object") { exports["ReactRouter"] = factory(require("react")); } else { root["ReactRouter"] = factory(root["React"]); }
3、非常重要的是一定要在output選項中加入如下一句話:
output: { libraryTarget: "umd" }
由于通過external提取過的js模塊是不會被記錄到webapck的chunk信息中,通過libraryTarget可告知我們構建出來的業務模塊,當讀到了externals中的key時,需要以umd的方式去獲取資源名,否則會有出現找不到module的情況。
通過配置后,我們可以看到對應的資源信息已經可以在瀏覽器的source map中讀到了。
對應的資源也可以直接由頁面外鏈載入,有效地減小了資源包的體積。
方案三、利用 DllPlugin 和 DllReferencePlugin 預編譯資源模塊我們的項目依賴中通常會引用大量的npm包,而這些包在正常的開發過程中并不會進行修改,但是在每一次構建過程中卻需要反復的將其解析,如何來規避此類損耗呢?這兩個插件就是干這個用的。
簡單來說DllPlugin的作用是預先編譯一些模塊,而DllReferencePlugin則是把這些預先編譯好的模塊引用起來。這邊需要注意的是DllPlugin必須要在DllReferencePlugin執行前先執行一次,dll這個概念應該也是借鑒了windows程序開發中的dll文件的設計理念。
相對于externals,dllPlugin有如下幾點優勢:
dll預編譯出來的模塊可以作為靜態資源鏈接庫可被重復使用,尤其適合多個項目之間的資源共享,如同一個站點pc和手機版等;
dll資源能有效地解決資源循環依賴的問題,部分依賴庫如:react-addons-css-transition-group這種原先從react核心庫中抽取的資源包,整個代碼只有一句話:
module.exports = require("react/lib/ReactCSSTransitionGroup");
卻因為重新指向了react/lib中,這也會導致在通過externals引入的資源只能識別react,尋址解析react/lib則會出現無法被正確索引的情況。
由于externals的配置項需要對每個依賴庫進行逐個定制,所以每次增加一個組件都需要手動修改,略微繁瑣,而通過dllPlugin則能完全通過配置讀取,減少維護的成本;
1、配置dllPlugin對應資源表并編譯文件
那么externals該如何使用呢,其實只需要增加一個配置文件:webpack.dll.config.js:
const webpack = require("webpack"); const path = require("path"); const isDebug = process.env.NODE_ENV === "development"; const outputPath = isDebug ? path.join(__dirname, "../common/debug") : path.join(__dirname, "../common/dist"); const fileName = "[name].js"; // 資源依賴包,提前編譯 const lib = [ "react", "react-dom", "react-router", "history", "react-addons-pure-render-mixin", "react-addons-css-transition-group", "redux", "react-redux", "react-router-redux", "redux-actions", "redux-thunk", "immutable", "whatwg-fetch", "byted-people-react-select", "byted-people-reqwest" ]; const plugin = [ new webpack.DllPlugin({ /** * path * 定義 manifest 文件生成的位置 * [name]的部分由entry的名字替換 */ path: path.join(outputPath, "manifest.json"), /** * name * dll bundle 輸出到那個全局變量上 * 和 output.library 一樣即可。 */ name: "[name]", context: __dirname }), new webpack.optimize.OccurenceOrderPlugin() ]; if (!isDebug) { plugin.push( new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }), new webpack.optimize.UglifyJsPlugin({ mangle: { except: ["$", "exports", "require"] }, compress: { warnings: false }, output: { comments: false } }) ) } module.exports = { devtool: "#source-map", entry: { lib: lib }, output: { path: outputPath, filename: fileName, /** * output.library * 將會定義為 window.${output.library} * 在這次的例子中,將會定義為`window.vendor_library` */ library: "[name]", libraryTarget: "umd", umdNamedDefine: true }, plugins: plugin };
然后執行命令:
$ NODE_ENV=development webpack --config webpack.dll.lib.js --progress $ NODE_ENV=production webpack --config webpack.dll.lib.js --progress
即可分別編譯出支持調試版和生產環境中lib靜態資源庫,在構建出來的文件中我們也可以看到會自動生成如下資源:
common ├── debug │ ├── lib.js │ ├── lib.js.map │ └── manifest.json └── dist ├── lib.js ├── lib.js.map └── manifest.json
文件說明:
lib.js可以作為編譯好的靜態資源文件直接在頁面中通過src鏈接引入,與externals的資源引入方式一樣,生產與開發環境可以通過類似charles之類的代理轉發工具來做路由替換;
manifest.json中保存了webpack中的預編譯信息,這樣等于提前拿到了依賴庫中的chunk信息,在實際開發過程中就無需要進行重復編譯;
2、dllPlugin的靜態資源引入
lib.js和manifest.json存在一一對應的關系,所以我們在調用的過程也許遵循這個原則,如當前處于開發階段,對應我們可以引入common/debug文件夾下的lib.js和manifest.json,切換到生產環境的時候則需要引入common/dist下的資源進行對應操作,這里考慮到手動切換和維護的成本,我們推薦使用add-asset-html-webpack-plugin進行依賴資源的注入,可得到如下結果:
在webpack.config.js文件中增加如下代碼:
const isDebug = (process.env.NODE_ENV === "development"); const libPath = isDebug ? "../dll/lib/manifest.json" : "../dll/dist/lib/manifest.json"; // 將mainfest.json添加到webpack的構建中 module.export = { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }) ] }
配置完成后我們能發現對應的資源包已經完成了純業務模塊的提取
多個工程之間如果需要使用共同的lib資源,也只需要引入對應的lib.js和manifest.js即可,plugin配置中也支持多個webpack.DllReferencePlugin同時引入使用,如下:
module.export = { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }), new webpack.DllReferencePlugin({ context: __dirname, manifest: require(ChartsPath), }) ] }方案四、使用 Happypack 加速你的代碼構建
以上介紹均為針對webpack中的chunk計算和編譯內容的優化與改進,對資源的實際體積改進上也較為明顯,那么除此之外,我們能否針對資源的編譯過程和速度優化上做些嘗試呢?
眾所周知,webpack中為了方便各種資源和類型的加載,設計了以loader加載器的形式讀取資源,但是受限于node的編程模型影響,所有的loader雖然以async的形式來并發調用,但是還是運行在單個 node的進程以及在同一個事件循環中,這就直接導致了當我們需要同時讀取多個loader文件資源時,比如babel-loader需要transform各種jsx,es6的資源文件。在這種同步計算同時需要大量耗費cpu運算的過程中,node的單進程模型就無優勢了,那么happypack就針對解決此類問題而生。
開啟happypack的線程池happypack的處理思路是將原有的webpack對loader的執行過程從單一進程的形式擴展多進程模式,原本的流程保持不變,這樣可以在不修改原有配置的基礎上來完成對編譯過程的優化,具體配置如下:
const HappyPack = require("happypack"); const os = require("os") const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length}); // 啟動線程池}); module:{ rules: [ { test: /.(js|jsx)$/, // use: ["babel-loader?cacheDirectory"], use: "happypack/loader?id=jsx", exclude: /^node_modules$/ } ] }, plugins:[ new HappyPack({ id: "jsx", cache: true, threadPool: HappyThreadPool, loaders: ["babel-loader"] }) ]
我們可以看到通過在loader中配置直接指向happypack提供的loader,對于文件實際匹配的處理 loader,則是通過配置在plugin屬性來傳遞說明,這里happypack提供的loader與plugin的銜接匹配,則是通過id=happybabel來完成。配置完成后,laoder的工作模式就轉變成了如下所示:
happypack在編譯過程中除了利用多進程的模式加速編譯,還同時開啟了cache計算,能充分利用緩存讀取構建文件,對構建的速度提升也是非常明顯的,經過測試,最終的構建速度提升如下:
優化前:
優化后:
關于happyoack的更多介紹可以查看:
happypack
happypack 原理解析
方案五、增強 uglifyPluginuglifyJS憑借基于node開發,壓縮比例高,使用方便等諸多優點已經成為了js壓縮工具中的首選,但是我們在webpack的構建中觀察發現,當webpack build進度走到80%前后時,會發生很長一段時間的停滯,經測試對比發現這一過程正是uglfiyJS在對我們的output中的bunlde部分進行壓縮耗時過長導致,針對這塊我們可以使用webpack-uglify-parallel來提升壓縮速度。
從插件源碼中可以看到,webpack-uglify-parallel的是實現原理是采用了多核并行壓縮的方式來提升我們的壓縮速度。
plugin.nextWorker().send({ input: input, inputSourceMap: inputSourceMap, file: file, options: options }); plugin._queue_len++; if (!plugin._queue_len) { callback(); } if (this.workers.length < this.maxWorkers) { var worker = fork(__dirname + "/lib/worker"); worker.on("message", this.onWorkerMessage.bind(this)); worker.on("error", this.onWorkerError.bind(this)); this.workers.push(worker); } this._next_worker++; return this.workers[this._next_worker % this.maxWorkers];
使用配置也非常簡單,只需要將我們原來webpack中自帶的uglifyPlugin配置:
new webpack.optimize.UglifyJsPlugin({ exclude:/.min.js$/ mangle:true, compress: { warnings: false }, output: { comments: false } })
修改成如下代碼即可:
const os = require("os"); const UglifyJsParallelPlugin = require("webpack-uglify-parallel"); new UglifyJsParallelPlugin({ workers: os.cpus().length, mangle: true, compressor: { warnings: false, drop_console: true, drop_debugger: true } })
目前webpack官方也維護了一個支持多核壓縮的UglifyJs插件:uglifyjs-webpack-plugin,使用方式類似,優勢在于完全兼容webpack.optimize.UglifyJsPlugin中的配置,可以通過uglifyOptions寫入,因此也做為推薦使用,參考配置如下:
const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); new UglifyJsPlugin({ uglifyOptions: { ie8: false, ecma: 8, mangle: true, output: { comments: false }, compress: { warnings: false } }, sourceMap: false, cache: true, parallel: os.cpus().length * 2 })方案六、Tree-shaking & Scope Hoisting
wepback在2.X和3.X中從rolluo中借鑒了tree-shaking和Scope Hoisting,利用es6的module特性,利用AST對所有引用的模塊和方法做了靜態分析,從而能有效地剔除項目中的沒有引用到的方法,并將相關方法調用歸納到了獨立的webpack_module中,對打包構建的體積優化也較為明顯,但是前提是所有的模塊寫法必須使用ES6 Module進行實現,具體配置參考如下:
// .babelrc: 通過配置減少沒有引用到的方法 { "presets": [ ["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }], // https://www.zhihu.com/question/41922432 ["es2015", {"modules": false}] // tree-shaking ] } // webpack.config: Scope Hoisting { plugins:[ // https://zhuanlan.zhihu.com/p/27980441 new webpack.optimize.ModuleConcatenationPlugin() ] }適用場景
在實際的開發過程中,可靈活地選擇適合自身業務場景的優化手段。
優化手段 | 開發環境 | 生產環境 |
---|---|---|
CommonsChunk | √ | √ |
externals | ? | √ |
DllPlugin | √ | √ |
Happypack | √ | ? |
uglify-parallel | ? | √ |
工程演示demo
溫馨提醒本文中的所有例子已經重新優化,支持最新的webpack3特性,并附帶有分享ppt地址,可以在線點擊查看
小結性能優化無小事,追求快沒有止境,在前端工程日益龐大復雜的今天,針對實際項目,持續改進構建工具的性能,對項目開發效率的提升和工具深度理解都是極其有益的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86594.html
摘要:而一個哈希字符串就是根據文件內容產生的簽名,每當文件內容發生更改時,哈希串也就發生了更改,文件名也就隨之更改。很顯然這不是我們需要的,如果文件內容發生了更改,的打包文件的哈希應該發生變化,但是不應該。前言 隨著前端代碼需要處理的業務越來越繁重,我們不得不面臨的一個問題是前端的代碼體積也變得越來越龐大。這造成無論是在調式還是在上線時都需要花長時間等待編譯完成,并且用戶也不得不花額外的時間和帶寬...
摘要:前端每周清單第期現狀分析與優化策略單元測試爬蟲作者王下邀月熊編輯徐川前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清單第 29 期:Web 現狀分析與優化策略...
摘要:感受構建工具給前端優化工作帶來的便利。多多益處邏輯清晰,程序注重數據與表現分離,可讀性強,利于規避和排查問題構建工具層出不窮。其實工具都能滿足需求,關鍵是看怎么用,工具的使用背后是對前端性能優化的理解程度。 這篇主要介紹一下我在玩Webpack過程中的心得。通過實例介紹WebPack的安裝,插件使用及加載策略。感受構建工具給前端優化工作帶來的便利。 showImg(https://se...
摘要:或者的,都會對其進行分析。舒適的開發體驗,有助于提高我們的開發效率,優化開發體驗也至關重要組件熱刷新熱刷新自從推出熱刷新后,前端開發者在開環境下體驗大幅提高。實現熱調試后,調試流程大幅縮短,和普通非直出模式調試體驗保持一致。 showImg(https://segmentfault.com/img/bVbtOR3?w=1177&h=635); webpack,打包所有的資源 不知道不...
閱讀 1459·2021-11-22 13:52
閱讀 1281·2021-09-29 09:34
閱讀 2690·2021-09-09 11:40
閱讀 3031·2019-08-30 15:54
閱讀 1255·2019-08-30 15:53
閱讀 971·2019-08-30 11:01
閱讀 1354·2019-08-29 17:22
閱讀 1943·2019-08-26 10:57