您好,登錄后才能下訂單哦!
前言
在前端應用的優化中,對加載資源的大小控制極其的重要,大多數時候我們能做的是在打包編譯的過程對資源進行大小控制、拆分與復用。
本片文章中主要是基于 webpack 打包,以 React、vue 等生態開發的單頁面應用來舉例說明如何從 webpack 打包的層面去處理資源以及緩存,其中主要我們需要做的是對 webpack 進行配置的優化,同時涉及少量的業務代碼的更改。
同時對打包資源的分析可以使用 (webpack-contrib/webpack-bundle-analyzer) 插件,當然可選的分析插件還是很多的,在本文中主要以該插件來舉例分析。
TIP: webpack 版本 @3.6.0
一、打包環境與代碼壓縮
首先我們有一個最基本的 webpack 配置:
// webpack.config.js const path = require('path'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js' }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin() ], resolve: { extensions: ['.js', '.jsx'] }, };
執行打包可以看到一個項目的 js 有 1M 以上:
Hash: e51afc2635f08322670b Version: webpack 3.6.0 Time: 2769ms Asset Size Chunks Chunk Names index.caa7.js 1.3 MB 0 [emitted] [big] index
這時候只需要增加插件 `DefinePlugin` 與 `UglifyJSPlugin` 即可減少不少的體積,在 plugins 中添加:
// webpack.config.js ... { ... plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }) ] ... }
可以看到這時候的打包輸出:
Hash: 84338998472a6d3c5c25 Version: webpack 3.6.0 Time: 9940ms Asset Size Chunks Chunk Names index.89c2.js 346 kB 0 [emitted] [big] index
代碼的大小從 1.3M 降到了 346K。
(1)DefinePlugin
DefinePlugin 允許創建一個在編譯時可以配置的全局常量。這可能會對開發模式和發布模式的構建允許不同的行為非常有用。如果在開發構建中,而不在發布構建中執行日志記錄,則可以使用全局常量來決定是否記錄日志。這就是 DefinePlugin 的用處,設置它,就可以忘記開發和發布構建的規則。
在我們的業務代碼和第三方包的代碼中很多時候需要判斷 `process.env.NODE_ENV` 來做不同處理,而在生產環境中我們顯然不需要非 `production` 的處理部分。
在這里我們設置 `process.env.NODE_ENV` 為 `JSON.stringify('production')`,也就是表示講打包環境設置為生產環境。之后配合 `UglifyJSPlugin` 插件就可以在給生產環境打包的時候去除部分的冗余代碼。
(2)UglifyJSPlugin
UglifyJSPlugin 主要是用于解析、壓縮 js 代碼,它基于 `uglify-es` 來對 js 代碼進行處理,它有多種配置選項:https://github.com/webpack-contrib/uglifyjs-webpack-plugin。
通過對代碼的壓縮處理以及去除冗余,大大減小了打包資源的體積大小。
二、代碼拆分/按需加載
在如 React 或者 vue 構建的單頁面應用中,對頁面路由與視圖的控制是由前端來實現的,其對應的業務邏輯都在 js 代碼中。
當一個應用設計的頁面和邏輯很多的時候,最終生成的 js 文件資源也會相當大。
然而當我們打開一個 url 對應的頁面時,實際上需要的并非全部的 js 代碼,所需要的僅是一個主的運行時代碼與該視圖對應的業務邏輯的代碼,在加載下一個視圖的時候再去加載那部分的代碼。
因此,對這方面可做的優化就是對 js 代碼進行按需加載。
懶加載或者按需加載,是一種很好的優化網頁或應用的方式。這種方式實際上是先把你的代碼在一些邏輯斷點處分離開,然后在一些代碼塊中完成某些操作后,立即引用或即將引用另外一些新的代碼塊。這樣加快了應用的初始加載速度,減輕了它的總體體積,因為某些代碼塊可能永遠不會被加載。
在 webpack 中提供了動態導入的技術來實現代碼拆分,首先在 webpack 的配置中需要去配置拆分出來的每個子模塊的配置:
// webpack.config.js const path = require('path'); const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js', chunkFilename: '[name].[chunkhash:4].child.js', }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), ], resolve: { extensions: ['.js', '.jsx'] }, };
其中主要需要定義的則是 `output` 中的 `chunkFilename`,它是導出的拆分代碼的文件名,這里給它設置為 `[name].[chunkhash:4].child.js`,其中的 `name` 對應模塊名稱或者 id,`chunkhash` 是模塊內容的 hash。
之后在業務代碼中 webpack 提供了兩種方式來動態導入:
對于最新版本的 webpack 主要推薦使用 `import()` 的方式(注意:import 使用到了 Promise,因此需要確保代碼中支持了 Promise 的 polyfill)。
// src/index.js function getComponent() { return import( /* webpackChunkName: "lodash" */ 'lodash' ).then(_ => { var element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }).catch(error => 'An error occurred while loading the component'); } getComponent().then(component => { document.body.appendChild(component); })
可以看到打包的信息:
Hash: d6ba79fe5995bcf9fa4d Version: webpack 3.6.0 Time: 7022ms Asset Size Chunks Chunk Names lodash.89f0.child.js 85.4 kB 0 [emitted] lodash index.316e.js 1.96 kB 1 [emitted] index [0] ./src/index.js 441 bytes {1} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden module
可以看到打包出來的代碼生成了 `index.316e.js` 和 `lodash.89f0.child.js` 兩個文件,后者則是通過 `import` 來實現拆分的。
`import` 它接收一個 `path` 參數,指的是該子模塊對于的路徑,同時還注意到其中可以添加一行注釋 `/* webpackChunkName: "lodash" */`,該注釋并非是無用的,它定義了該子模塊的 name,其對應與 `output.chunkFilename` 中的 `[name]`。
`import` 函數返回一個 Promise,當異步加載到子模塊代碼是會執行后續操作,比如更新視圖等。
(1)React 中的按需加載
在 React 配合 React-Router 開發中,往往就需要代碼根據路由按需加載的能力,下面是一個基于 webpack 代碼動態導入技術實現的 React 動態載入的組件:
import React, { Component } from 'react'; export default function lazyLoader (importComponent) { class AsyncComponent extends Component { state = { Component: null } async componentDidMount () { const { default: Component } = await importComponent(); this.setState({ Component: Component }); } render () { const Component = this.state.Component; return Component ? <Component {...this.props} /> : null; } } return AsyncComponent; };
在 `Route` 中:
<Switch> <Route exact path="/" component={lazyLoader(() => import('./Home'))} /> <Route path="/about" component={lazyLoader(() => import('./About'))} /> <Route component={lazyLoader(() => import('./NotFound'))} /> </Switch>
在 `Route` 中渲染的是 `lazyLoader` 函數返回的組件,該組件在 mount 之后會去執行 `importComponent` 函數(既:`() => import('./About')`)動態加載其對于的組件模塊(被拆分出來的代碼),待加載成功之后渲染該組件。
使用該方式打包出來的代碼:
Hash: 02a053d135a5653de985 Version: webpack 3.6.0 Time: 9399ms Asset Size Chunks Chunk Names 0.db22.child.js 5.82 kB 0 [emitted] 1.fcf5.child.js 4.4 kB 1 [emitted] 2.442d.child.js 3 kB 2 [emitted] index.1bbc.js 339 kB 3 [emitted] [big] index
三、抽離 Common 資源
(1)第三方庫的長緩存
首先對于一些比較大的第三方庫,比如在 React 中用到的 react、react-dom、react-router 等等,我們不希望它們被重復打包,并且在每次版本更新的時候也不希望去改變這部分的資源導致在用戶端重新加載。
在這里可以使用 webpack 的 CommonsChunkPlugin 來抽離這些公共資源;
CommonsChunkPlugin 插件,是一個可選的用于建立一個獨立文件(又稱作 chunk)的功能,這個文件包括多個入口 chunk 的公共模塊。通過將公共模塊拆出來,最終合成的文件能夠在最開始的時候加載一次,便存起來到緩存中供后續使用。這個帶來速度上的提升,因為瀏覽器會迅速將公共的代碼從緩存中取出來,而不是每次訪問一個新頁面時,再去加載一個更大的文件。
首先需要在 entry 中新增一個入口用來打包需要抽離出來的庫,這里將 `'react', 'react-dom', 'react-router-dom', 'immutable'` 都給單獨打包進 `vendor` 中;
之后在 plugins 中定義一個 `CommonsChunkPlugin` 插件,同時將其 `name` 設置為 `vendor` 是它們相關聯,再將 `minChunks` 設置為 `Infinity` 防止其他代碼被打包進來。
// webpack.config.js const path = require('path'); const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const PROJECT_ROOT = path.resolve(__dirname, './'); module.exports = { entry: { index: './src0/index.js', vendor: ['react', 'react-dom', 'react-router-dom', 'immutable'] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash:4].js', chunkFilename: '[name].[chunkhash:4].child.js', }, module: { rules: [ { test: /\.js[x]?$/, use: 'babel-loader', include: PROJECT_ROOT, exclude: /node_modules/ } ] }, plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity, }), ], resolve: { extensions: ['.js', '.jsx'] }, };
運行打包可以看到:
Hash: 34a71fcfd9a24e810c21 Version: webpack 3.6.0 Time: 9618ms Asset Size Chunks Chunk Names 0.2c65.child.js 5.82 kB 0 [emitted] 1.6e26.child.js 4.4 kB 1 [emitted] 2.e4bc.child.js 3 kB 2 [emitted] index.4e2f.js 64.2 kB 3 [emitted] index vendor.5fd1.js 276 kB 4 [emitted] [big] vendor
可以看到 `vendor` 被單獨打包出來了。
當我們改變業務代碼時再次打包:
Hash: cd3f1bc16b28ac97e20a Version: webpack 3.6.0 Time: 9750ms Asset Size Chunks Chunk Names 0.2c65.child.js 5.82 kB 0 [emitted] 1.6e26.child.js 4.4 kB 1 [emitted] 2.e4bc.child.js 3 kB 2 [emitted] index.4d45.js 64.2 kB 3 [emitted] index vendor.bc85.js 276 kB 4 [emitted] [big] vendor
vendor 包同樣被打包出來的,然而它的文件 hash 卻發生了變化,這顯然不符合我們長緩存的需求。
這是因為 webpack 在使用 CommoChunkPlugin 的時候會生成一段 runtime 代碼(它主要用來處理代碼模塊的映射關系),而哪怕沒有改變 vendor 里的代碼,這個 runtime 仍然是會跟隨著打包變化的并且打入 verdor 中,所以 hash 就會開始變化了。解決方案則是把這部分的 runtime 代碼也單獨抽離出來,修改之前的 `CommonsChunkPlugin` 為:
// webpack.config.js ... new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), ...
執行打包可以看到生成的代碼中多了 `runtime` 文件,同時即使改變業務代碼,vendor 的 hash 值也保持不變了。
當然這段 `runtime` 實際上非常短,我們可以直接 inline 在 html 中,如果使用的是 `html-webpack-plugin` 插件處理 html,則可以結合 [`html-webpack-inline-source-plugin`](DustinJackson/html-webpack-inline-source-plugin) 插件自動處理其 inline。
(2)公共資源抽離
在我們打包出來的 js 資源包括不同入口以及子模塊的 js 資源包,然而它們之間也會重復載入相同的依賴模塊或者代碼,因此可以通過 CommonsChunkPlugin 插件將它們共同依賴的一些資源打包成一個公共的 js 資源。
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // ( 公共chunk(commnons chunk) 的名稱) name: "commons", // ( 公共chunk 的文件名) filename: "commons.[chunkhash:4].js", // (模塊必須被 3個 入口chunk 共享) minChunks: 3 }) ],
可以看到這里增加了 `commons` 的一個打包,當一個資源被三個以及以上 chunk 依賴時,這些資源會被單獨抽離打包到 `commons.[chunkhash:4].js` 文件。
執行打包,看到結果如下:
Hash: 2577e42dc5d8b94114c8 Version: webpack 3.6.0 Time: 24009ms Asset Size Chunks Chunk Names 0.2eee.child.js 90.8 kB 0 [emitted] 1.cfbc.child.js 89.4 kB 1 [emitted] 2.557a.child.js 88 kB 2 [emitted] vendor.66fd.js 275 kB 3 [emitted] [big] vendor index.688b.js 64.2 kB 4 [emitted] index commons.a61e.js 1.78 kB 5 [emitted] commons
卻發現這里的 `commons.[chunkhash].js` 基本沒有實際內容,然而明明在每個子模塊中也都依賴了一些相同的依賴。
借助 webpack-bundle-analyzer 來分析一波:
可以看到三個模塊都依賴了 `lodash`,然而它并沒有被抽離出來。
這是因為 CommonsChunkPlugin 中的 chunk 指的是 entry 中的每個入口,因此對于一個入口拆分出來的子模塊(children chunk)是不生效的。
可以通過在 CommonsChunkPlugin 插件中配置 `children` 參數將拆分出來的子模塊的公共依賴也打包進 `commons` 中:
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // ( 公共chunk(commnons chunk) 的名稱) name: "commons", // ( 公共chunk 的文件名) filename: "commons.[chunkhash:4].js", // (模塊必須被 3個 入口chunk 共享) minChunks: 3 }), new webpack.optimize.CommonsChunkPlugin({ // (選擇所有被選 chunks 的子 chunks) children: true, // (在提取之前需要至少三個子 chunk 共享這個模塊) minChunks: 3, }) ],
查看打包效果:
其子模塊的公共資源都被打包到 `index` 之中了,并沒有理想地打包進 `commons` 之中,還是因為 `commons` 對于的是 entry 中的入口模塊,而這里并未有 3 個 entry 模塊共用資源;
在單入口的應用中可以選擇去除 `commons`,而在子模塊的 `CommonsChunkPlugin` 的配置中配置 `async` 為 `true`:
// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity, }), new webpack.optimize.CommonsChunkPlugin({ // (選擇所有被選 chunks 的子 chunks) children: true, // (異步加載) async: true, // (在提取之前需要至少三個子 chunk 共享這個模塊) minChunks: 3, }) ],
查看效果:
子模塊的公共資源都被打包到 `0.9c90.child.js` 中了,該模塊則是子模塊的 commons。
四、tree shaking
tree shaking 是一個術語,通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統中的靜態結構特性,例如 import 和 export。這個術語和概念實際上是興起于 ES2015 模塊打包工具 rollup。
在我們引入一個依賴的某個輸出的時候,我們可能需要的僅僅是該依賴的某一部分代碼,而另一部分代碼則是 `unused` 的,如果能夠去除這部分代碼,那么最終打包出來的資源體積也是可以有可觀的減小。
首先,webpack 中實現 tree shaking 是基于 webpack 內部支持的 es2015 的模塊機制,在大部分時候我們使用 babel 來編譯 js 代碼,而 babel 會通過自己的模塊加載機制處理一遍,這導致 webpack 中的 tree shaking 處理將會失效。因此在 babel 的配置中需要關閉對模塊加載的處理:
// .babelrc { "presets": [ [ "env", { "modules": false, } ], "stage-0" ], ... }
然后我們來看下 webpack 是如何處理打包的代碼,舉例有一個入口文件 `index.js` 和一個 `utils.js` 文件:
// utils.js export function square(x) { return x * x; } export function cube(x) { return x * x * x; } ``` ```js // index.js import { cube } from './utils.js'; console.log(cube(10)); ``` 打包出來的代碼: ``` // index.bundle.js /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* unused harmony export square */ /* harmony export (immutable) */ __webpack_exports__["a"] = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }
可以看到僅有 `cube` 函數被 `__webpack_exports__` 導出來,而 `square` 函數被標記為 `unused harmony export square`,然而在打包代碼中既是 `square` 沒有被導出但是它仍然存在與代碼中,而如何去除其代碼則可以通過添加 `UglifyjsWebpackPlugin` 插件來處理。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。