Dans l'optimisation des applications frontales, il est extrêmement important de contrôler la taille des ressources chargées. La plupart du temps, ce que nous pouvons faire est de contrôler la taille, de diviser et de réutiliser les ressources pendant le processus de packaging et de compilation. Cet article présente principalement comment utiliser webpack pour optimiser les ressources. L'éditeur pense que c'est plutôt bien, je vais donc le partager avec vous maintenant et le donner comme référence. Suivons l'éditeur pour y jeter un œil, j'espère que cela pourra aider tout le monde.
Avant-propos
Cet article est principalement basé sur l'empaquetage du webpack, en utilisant des applications d'une seule page développées dans React, Vue et d'autres écosystèmes comme exemples pour illustrer comment gérer les ressources et le cache à partir du niveau d'empaquetage du webpack. La principale chose que nous devons faire est d'optimiser la configuration du webpack et d'impliquer également une petite quantité de modifications du code métier.
En même temps, le plug-in (webpack-contrib/webpack-bundle-analyzer) peut être utilisé pour analyser les ressources packagées. Bien sûr, il existe de nombreux plug-ins d'analyse facultatifs dans cet article. ce plug-in est principalement utilisé à titre d'exemple.
CONSEIL : version du webpack @3.6.0
1. Environnement d'empaquetage et compression du code
Nous avons d'abord une configuration de base du 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'] }, };
Après avoir exécuté le package, vous pouvez constater que le js d'un projet fait plus de 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
À ce stade, il vous suffit d'ajouter les plug-ins `DefinePlugin` et `UglifyJSPlugin` pour réduire beaucoup de volume. Ajoutez des 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 }, } }) ] ... }
Vous pouvez voir la sortie de l'emballage à ce moment :
Hash: 84338998472a6d3c5c25 Version: webpack 3.6.0 Time: 9940ms Asset Size Chunks Chunk Names index.89c2.js 346 kB 0 [emitted] [big] index
La taille du code a été réduite de 1,3 Mo à 346 Ko.
(1) DefinePlugin
DefinePlugin permet la création d'une constante globale configurable au moment de la compilation. Cela peut être utile pour permettre des comportements différents pour les builds en mode développement et en mode release. Si vous effectuez une journalisation dans une version de développement, mais pas dans une version de version, vous pouvez utiliser des constantes globales pour décider de journaliser ou non. C'est là qu'intervient DefinePlugin, configurez-le et oubliez les règles de développement et de versions.
Dans notre code métier et le code de package tiers, nous devons souvent juger `process.env.NODE_ENV` pour différents traitements, mais dans l'environnement de production, nous n'avons évidemment pas besoin de non-`production `partie de traitement.
Ici, nous définissons `process.env.NODE_ENV` sur `JSON.stringify('production')`, ce qui signifie que l'environnement d'empaquetage est défini sur l'environnement de production. Ensuite, avec le plug-in `UglifyJSPlugin`, vous pouvez supprimer du code redondant lors du packaging pour l'environnement de production.
(2) UglifyJSPlugin
UglifyJSPlugin est principalement utilisé pour analyser et compresser le code js Il est basé sur `uglify-es` pour traiter le code js. : //github.com/webpack-contrib/uglifyjs-webpack-plugin.
En compressant le code et en supprimant la redondance, la taille des ressources packagées est considérablement réduite.
2. Fractionnement du code/chargement à la demande
Dans les applications monopage construites avec React ou vue, le contrôle du routage des pages et des vues est implémenté par le front-end et son Business correspondant. la logique est entièrement dans le code js.
Lorsqu'une conception d'application comporte de nombreuses pages et logiques, les ressources du fichier js final généré seront également assez volumineuses.
Cependant, lorsque nous ouvrons une page correspondant à une URL, tout le code js n'est pas réellement nécessaire. Il suffit d'un code d'exécution principal et du code de logique métier correspondant à la vue. cette partie du code dans la vue suivante.
Par conséquent, l'optimisation qui peut être faite à cet égard est de charger du code js à la demande.
Le chargement paresseux ou chargement à la demande est un bon moyen d'optimiser les pages Web ou les applications. Cette méthode sépare en fait votre code à certains points d'arrêt logiques, puis, après avoir effectué certaines opérations dans certains blocs de code, fait immédiatement référence ou est sur le point de référencer d'autres nouveaux blocs de code. Cela accélère le chargement initial de l'application et réduit sa taille globale, puisque certains blocs de code peuvent ne jamais être chargés.
Webpack fournit une technologie d'importation dynamique pour réaliser le fractionnement du code. Tout d'abord, dans la configuration de webpack, vous devez configurer la configuration de chaque sous-module fractionné :
// 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'] }, };
Le. La principale chose qui doit être définie est le « chunkFilename » dans « output », qui est le nom de fichier du code divisé exporté. Ici, il est défini sur « [name].[chunkhash:4].child.js`, Le. `name` correspond au nom ou à l'identifiant du module, et `chunkhash` est le hachage du contenu du module.
Après cela, webpack propose deux façons d'importer dynamiquement dans le code métier :
`import('path/to/module') ->
`require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)`
Pour la dernière version de webpack, il est principalement recommandé d'utiliser `import()` (remarque : l'importation utilise Promise, vous devez donc vous assurer que le code prend en charge le polyfill Promise).
// src/index.js function getComponent() { return import( /* webpackChunkName: "lodash" */ 'lodash' ).then(_ => { var element = document.createElement('p'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }).catch(error => 'An error occurred while loading the component'); } getComponent().then(component => { document.body.appendChild(component); })
Vous pouvez voir les informations d'emballage :
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
Vous pouvez voir que le code empaqueté a généré `index.316e.js` et `lodash.89f0.child.js ` Deux fichiers, ce dernier est divisé via « importation ».
`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` 插件来处理。
相关推荐:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!