讓你的Webpack起飛—考拉會員後台Webpack優化實戰

讓你的Webpack起飛—考拉會員後台Webpack優化實戰

425 人贊了文章

最近看到了美團的前端團隊的一篇文章,文中提到前端發布僅需10秒,默默的看了一下我們自己的發布時間。。。

先定一個小目標,爭取把 Webpack 的打包時間優化到10秒以內吧。

先看一下現在打包一次需要的時間,71164ms,下面開始一步一步見證奇蹟:

0. 性能分析

0.1 可視化分析

有很多工具提供了可視化的分析,如Webpack-bundle-analyzer、webpack-chart、 webpack-analyse。 以Webpack-bundle-analyzer為例,它提供了一個下圖所示的圖表,展示了引入的所有模塊的大小、路徑等信息,可以針對性的做出優化。

使用上也很簡單:

// 安裝:npm install webpack-bundle-analyzer --save-dev// webpack.config.js 配置const BundleAnalyzerPlugin = require(webpack-bundle-analyzer).BundleAnalyzerPlugin;plugins: [ new BundleAnalyzerPlugin({ analyzerMode: server, analyzerHost: 127.0.0.1, analyzerPort: 8888, reportFilename: report.html, defaultSizes: parsed, openAnalyzer: true, generateStatsFile: false, statsFilename: stats.json, logLevel: info })]

運行webpack命令,會自動在瀏覽器中打開http://127.0.0.1:8888/頁面,展示可視化圖表。

0.2 慢在哪裡

我們可以使用 Speed Measure Plugin 來對打包過程中消耗的時間進行精確的統計,如圖所示:

使用方式如下:

npm install speed-measure-webpack-plugin --save-devconst SpeedMeasurePlugin = require("speed-measure-webpack-plugin");const smp = new SpeedMeasurePlugin();const webpackConfig = smp.wrap({ plugins: [ new MyPlugin(), new MyOtherPlugin() ]});

1. 升級到 Weback4.x

Webpack4 帶來了極大的性能提升,按照開發者博客中的說法,構建速度最多甚至有高達98%的提升。

升級過程中遇到了一些網上的「Webpack4升級指南」等文章中沒有列出的問題,在此分享一下:

1.1 升級 Vue-loader

Vue-loader 目前最新版本為 v15.3.0,使用方式有了很大不同。 現在,我們需要引入一個新的插件 VueLoaderPlugin ,具體使用方式如下:

// webpack.config.jsconst VueLoaderPlugin = require(vue-loader/lib/plugin)module.exports = { plugins: [ new VueLoaderPlugin() ]}

同時,在 v15版本的 vue-loader 中,不再需要單獨為 .vue 組件中的模板、CSS等內容單獨配置 loader,可以共用普通文件的配置,如下所示:

// webpack.config.jsmodule: { rules: [{ test: /.vue$/, loader: vue-loader }, { // 它會應用到普通的 `.js` 文件 // 以及 `.vue` 文件中的 `<script>` 塊 test: /.js$/, loader: babel-loader }, { // 它會應用到普通的 `.css` 文件 // 以及 `.vue` 文件中的 `<style>` 塊 test: /.css$/, use: [ vue-style-loader, css-loader ] }]}

1.2 升級 Vue-router

在 vue-loader@13.0.0 版本中對模塊導入做了更新,為了支持 Webpack3 中的 Scope Hoisting 屬性,esModule 被默認設置為了 true,我們的 require 語法也需要做出相應修改,如下所示:

const Foo = require(./Foo.vue)// 需要改為const Foo = require(./Foo.vue).default

同理,在低於 Vue 2.4 和 vue-router 2.7 的版本中,import 語法也需要修改:

const Foo = () => import(./Foo.vue)// 需要改為const Foo = () => import(./Foo.vue).then(m => m.default)

詳情可以參考github.com/vuejs/vue-lo

1.3 Chunk 的命名

如果使用 webpackchunkname 魔法注釋來命名,需要注意 .babelrc 中 comment 必須為true。

注釋語法如下:

// 註:webpackChunkName 相同的組件會被打包在同個非同步塊 (chunk) 中。const Foo = () => import(/* webpackChunkName: "group-foo" */ ./Foo.vue)const Bar = () => import(/* webpackChunkName: "group-foo" */ ./Bar.vue)const Baz = () => import(/* webpackChunkName: "group-foo" */ ./Baz.vue)

1.4 提取 CSS 文件

在 Webpack4 環境下 extract-text-webpack-plugin 需要安裝 @next 版本,我們這裡直接使用了 mini-css-extract-plugin 來代替。

同時,在 mode 為 development 時,NamedChunksPluginNamedModulesPlugin 會默認開啟,不需要再顯式指定。

1.5 本地 mock 處理

2.x 版本的 Vue-Cli 啟動了一個 express 服務來處理本地數據的 mock,我們嘗試做了一些簡化,在服務啟動過程中遍曆本地數據文件生成 mock/index.js 文件,在 webpack-dev-serverbefore 方法中,使用 webpack-api-mocker 插件攔截了請求,讀取本地的 mock 數據(JSON文件)返回。

// webpack.dev.conf.jsconst apiMocker = require(webpack-api-mocker);require(./mock-generator.js)();devServer: { before(app) { apiMocker(app, path.resolve(./mock/index.js)); }}// mock/index.jsconst fs = require(fs);function fromJSONFile(filepath) { return (req, res) => { const data = fs.readFileSync(mock + filepath).toString(); const json = JSON.parse(data); return res.json(json); };};const proxy = { GET /aaa/bbb: fromJSONFile(/aaa/bbb.json), GET /aaa/ccc: fromJSONFile(/aaa/ccc.json)};module.exports = proxy;

Webpack4 及相關依賴升級完成之後,打包時間直接減少了半分鐘,達到了44.534秒,離小目標還有很大距離,我們繼續。

2. 路由處理(非同步載入)

在前面的打包完成的圖片中,我們可以看到生成了大量的文件,統計了一下,體積總計高達22.07M,文件近60個。 雖然提取非同步模塊有利於提升頁面初始化性能,但打包非同步模塊本身也是有成本的,經權衡我們決定僅對一級路由模塊作非同步處理。 具體實現如下:

Vue.use(Router);import Vue from vue;import Router from vue-router;export default new Router({routes: [{ path: /a, component: () => import ( /* webpackChunkName: a */ @/pages/a), name: a}, { path: /b, component: () => import ( /* webpackChunkName: b */ @/pages/b), name: b, children: [{ path: b/m, component: require(@/pages/b/m).default, name: m, children: [{ path: b/m/p, component: require(@/pages/b/m/p).default, name: p }, { path: b/m/q, component: require(@/pages/b/m/q).default, name: q }, ] }]}]

經過上面的優化,我們將生成的js文件數量減少到了8個,大小減小到了5M. 來看一下打包時間:21.959秒!距離小目標越來越近了。

3. HappyPack/thread-loader

HappyPack 可以將原有的 webpack 對 loader 的執行過程,從單一進程的形式擴展為多進程的模式,從而加速代碼構建。使用方式如下:

const HappyPack = require(happypack);const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });module: { loaders: [{ test: /.less$/, loader: ExtractTextPlugin.extract( style, path.resolve(__dirname, ./node_modules, happypack/loader) + ?id=less ) }]},plugins: [ new HappyPack({ id: less, loaders: [css!less], threadPool: happyThreadPool, cache: true, verbose: true })]

經過測試,在我們的項目中,對 js 和 ts 文件使用 happypack 收益最大。

此處需要注意的是,Vue-loader 不支持 happypack,可以使用 thread-loader 來進行加速,同樣是新建一個進程來執行 loader 操作,使用方式也很簡單:

module: { rules: [{ test: /.vue$/, use: [ thread-loader, vue-loader ] }]}

但是在我們的項目中,經過測試,thread-loader 對於打包速度幾乎沒有影響,是因為它本身的額外開銷導致,建議只在極高性能消耗的場景下使用。

完成之後,測試一下打包時間:15.101秒。

4. 緩存loader的執行結果(cacheDirectory/cache-loader)

我們可以對loader做如下配置來開啟緩存:

loader: babel-loader?cacheDirectory=true

或者我們也可以使用 cache-loader :

rules: [{ test: /.vue$/, use: [ cache-loader, vue-loader ]}]

加入緩存之後,再次測試打包時間:13.915秒。

5. 模塊進一步細分(splitChunks)

在 Webpack4 中移除了我們此前常用的 CommonsChunkPlugin 插件,取而代之的是 splitChunks 。 splitChunks 的默認配置已經足夠我們日常使用,沒有特殊需求可以不必特意處理。 我們此處的配置如下(生產環境):

optimization: { splitChunks: { cacheGroups: { commons: { test: /[\/]node_modules[\/]/, name: vendors, chunks: all }, // styles: { // name: index, // test: /.stylus|css$/, // chunks: all, // enforce: true // } } }}

其中,commons 部分的作用是分離出 node_modules 中引入的模塊,styles 部分則是合併 CSS 文件。 經過測試,在我們的項目中,styles 部分使構建時間增加了大約2秒,因此我們放棄了這部分操作。

6. 使用DllPlugin拆分模塊

開發過程中,我們經常需要引入大量第三方庫,這些庫並不需要隨時修改或調試,我們可以使用DllPlugin和DllReferencePlugin單獨構建它們。 具體使用如下:

const HtmlWebpackPlugin = require(html-webpack-plugin);module.exports = { entry: { vendor: [ axios, vue-i18n, vue-router, vuex ] }, output: { path: path.resolve(__dirname, ../static/), filename: [name].dll.js, library: [name]_library }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, build, [name]-manifest.json), name: [name]_library }) ]}

執行webpack命令,build目錄下即可生成 dll.js 文件和對應的 manifest 文件,使用 DLLReferencePlugin 引入:

plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(./build/vendor-manifest.json) })]

由於我們的項目中原本已經通過這種方式打包了大部分第三方庫,所以這裡對打包速度的提升不大,僅僅提升2秒,來到了11.509秒。

7. 精簡不必要的模塊

在我們的項目中,引入了一些如 moment、lodash 等重型庫,然而他們提供的絕大部分功能都是我們不需要的,權衡之後,我們移除了他們,自己實現了部分功能或使用了更小體積的庫代替。

移除了這些庫之後,我們的打包時間來到了8.921秒!小目標達成了~

但是這距離10秒發布還不夠,我們需要爭取壓縮出更多時間留給發布系統。

還能不能繼續提升呢?答案是肯定的。

8. 優化模塊查找路徑

Node.js的模塊的載入及緩存機制如下:

載入內置模塊 載入文件模塊 載入文件目錄模塊 載入node_modules里的模塊 自動緩存已載入模塊 如果模塊名不是路徑,也不是內置模塊,Node將試圖去當前目錄的node_modules文件夾里搜索。如果當前目錄的node_modules里沒有找到,Node會從父目錄的node_modules里搜索,這樣遞歸下去直到根目錄。

我們可以對搜索過程進行一些優化,比如可以像下面這樣指定路徑:

exclude: /node_modules/, // 排除不處理的目錄include: path.resolve(__dirname, src) // 精確指定要處理的目錄resolve: { modules: [path.resolve(__dirname, node_modules)], // 指定node_modules的位置 alias: { api: resolve(src/api) // 創建別名 }}

我們再來看一下時間:7.66秒!

到這裡,我們的這次優化基本完成了,其實還有很多可以優化的空間,比如升級一顆 i9 處理器~ 這裡也只是列舉出了一些常見的收益較大的優化方式,希望能對大家有一點幫助,也歡迎有興趣的同學一起交流。


推薦閱讀:

電動汽車電池使用氟化電解質解決長距離行駛問題
中日深度學習研究和應用的比較(譯文)
為什麼沒有國產廠商做電磁筆的旗艦?
來自天頂星 適馬黑科技dp1Quattro試用|適馬|dp1Quattro
矽谷科技霸權遭遇中國強勁挑戰

TAG:科技 | 前端開發 | 性能優化 |