专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  GPU:DeepSeek ... ·  23 小时前  
程序员的那些事  ·  印度把 DeepSeek ... ·  3 天前  
程序员小灰  ·  清华大学《DeepSeek学习手册》(全5册) ·  3 天前  
OSC开源社区  ·  2024: 大模型背景下知识图谱的理性回归 ·  4 天前  
程序员小灰  ·  DeepSeek做AI代写,彻底爆了! ·  6 天前  
51好读  ›  专栏  ›  SegmentFault思否

使用 webpack 优化资源

SegmentFault思否  · 公众号  · 程序员  · 2017-10-23 08:00

正文

前言 在前端应用的优化中,对加载资源的大小控制极其的重要,大多数时候我们能做的是在打包编译的过程对资源进行大小控制、拆分与复用。

本片文章中主要是基于 webpack 打包,以 React、vue 等生态开发的单页面应用来举例说明如何从 webpack 打包的层面去处理资源以及缓存,其中主要我们需要做的是对 webpack 进行配置的优化,同时涉及少量的业务代码的更改。

同时对打包资源的分析可以使用 (webpack-contrib/webpack-bundle-analyzer) 插件,当然可选的分析插件还是很多的,在本文中主要以该插件来举例分析。

TIP: webpack 版本 @3.6.0

一、打包环境与代码压缩

首先我们有一个最基本的 webpack 配置:

  1. // webpack.config.js

  2. const path = require('path');

  3. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

  4. const PROJECT_ROOT = path.resolve(__dirname, './');

  5. module.exports = {

  6.  entry: {

  7.    index: './src/index.js'

  8.  },

  9.  output : {

  10.    path: path.resolve(__dirname, 'dist'),

  11.    filename: '[name].[chunkhash:4].js'

  12.  },

  13.  module: {

  14.    rules: [

  15.      {

  16.        test: /\.js[x]?$/,

  17.        use: 'babel-loader',

  18.        include: PROJECT_ROOT,

  19.        exclude : /node_modules/

  20.      }

  21.    ]

  22.  },

  23.  plugins: [

  24.    new BundleAnalyzerPlugin()

  25.  ],

  26.  resolve: {

  27.    extensions: ['.js', '.jsx']

  28.  },

  29. };

执行打包可以看到一个项目的 js 有 1M 以上:

  1. Hash : e51afc2635f08322670b

  2. Version: webpack 3.6.0

  3. Time: 2769ms

  4.        Asset    Size  Chunks                    Chunk Names

  5. index.caa7.js  1.3 MB       0  [emitted]  [big]  index

这时候只需要增加插件 DefinePlugin UglifyJSPlugin 即可减少不少的体积,在 plugins 中添加:

  1. // webpack.config.js

  2. ...

  3. {

  4.   ...

  5.  plugins: [

  6.    new BundleAnalyzerPlugin(),

  7.    new webpack.DefinePlugin({

  8.      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')

  9.    }),

  10.    new UglifyJSPlugin({

  11.      uglifyOptions: {

  12.        ie8: false,

  13.        output: {

  14.          comments : false,

  15.          beautify: false,

  16.        },

  17.        mangle: {

  18.          keep_fnames: true

  19.        },

  20.        compress: {

  21.          warnings: false,

  22.          drop_console: true

  23.        },

  24.      }

  25.     })

  26.  ]

  27.  ...

  28. }

可以看到这时候的打包输出:

  1. Hash: 84338998472a6d3c5c25

  2. Version: webpack 3.6.0

  3. Time: 9940ms

  4.        Asset    Size  Chunks                    Chunk Names

  5. 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 的配置中需要去配置拆分出来的每个子模块的配置:

  1. // webpack.config.js

  2. const path = require('path');

  3. const webpack = require('webpack');

  4. const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

  5. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

  6. const PROJECT_ROOT = path.resolve(__dirname, './');

  7. module.exports = {

  8.  entry: {

  9.    index: './src/index.js'

  10.  },

  11.  output: {

  12.    path: path.resolve(__dirname, 'dist'),

  13.    filename: '[name].[chunkhash:4].js',

  14.    chunkFilename : '[name].[chunkhash:4].child.js',

  15.  },

  16.  module: {

  17.    rules: [

  18.      {

  19.        test: /\.js[x]?$/,

  20.        use: 'babel-loader',

  21.        include: PROJECT_ROOT,

  22.        exclude : /node_modules/

  23.      }

  24.    ]

  25.  },

  26.  plugins: [

  27.    new BundleAnalyzerPlugin(),

  28.    new webpack.DefinePlugin({

  29.      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')

  30.     }),

  31.    new UglifyJSPlugin({

  32.      uglifyOptions: {

  33.        ie8: false,

  34.        output: {

  35.          comments: false,

  36.          beautify: false,

  37.        },

  38.        mangle : {

  39.          keep_fnames: true

  40.        },

  41.        compress: {

  42.          warnings: false,

  43.          drop_console: true

  44.        },

  45.      }

  46.    }),

  47.   ],

  48.  resolve: {

  49.    extensions: ['.js', '.jsx']

  50.  },

  51. };

其中主要需要定义的则是 output 中的 chunkFilename ,它是导出的拆分代码的文件名,这里给它设置为 [ name ].[ chunkhash : 4 ]. child . js ,其中的 name 对应模块名称或者 id, chunkhash 是模块内容的 hash。

之后在业务代码中 webpack 提供了两种方式来动态导入:

  • import ( 'path/to/module' ) -& gt ; Promise

  • require . ensure ( dependencies : String [], callback : function ( require ), errorCallback : function ( error ), chunkName : String ) 对于最新版本的 webpack 主要推荐使用 import () 的方式(注意:import 使用到了 Promise,因此需要确保代码中支持了 Promise 的 polyfill)。

  1. // src/index.js

  2. function getComponent() {

  3.  return import(

  4.    /* webpackChunkName: "lodash" */

  5.    'lodash'

  6.  ).then(_ => {

  7.    var element = document.createElement('div');

  8.    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  9.    return element;

  10.  }).catch(error => 'An error occurred while loading the component');

  11. }

  12. getComponent().then(component => {

  13.  document.body.appendChild(component);

  14. })

可以看到打包的信息:

  1. Hash : d6ba79fe5995bcf9fa4d

  2. Version: webpack 3.6.0

  3. Time: 7022ms

  4.               Asset     Size  Chunks             Chunk Names

  5. lodash.89f0.child.js  85.4 kB       0  [emitted]  lodash

  6.       index .316e.js  1.96 kB       1  [emitted]  index

  7.   [0] ./src/index.js 441 bytes {1} [built]

  8.   [2] (webpack)/buildin/global.js 509 bytes {0} [built]

  9.   [3] (webpack)/buildin/module.js 517 bytes {0} [built]

  10.    + 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 动态载入的组件:

  1. import React, { Component } from 'react';

  2. export default function lazyLoader (importComponent) {

  3.  class AsyncComponent extends Component {

  4.    state = { Component: null }

  5.     async componentDidMount () {

  6.      const { default: Component } = await importComponent();

  7.      this.setState({

  8.         Component: Component

  9.      });

  10.    }

  11.    render () {

  12.       const Component = this.state.Component;

  13.      return Component

  14.        ? <Component {...this. props} />

  15.        : null;

  16.    }

  17.  }

  18.  return AsyncComponent;

  19. };

Route 中:

  1.   exact path="/"

  2.    component={lazyLoader(() => import('./Home'))}

  3.  />

  4.   path="/about"

  5.    component={lazyLoader(() => import('./About'))}

  6.  />

  7.  

  8.     component={lazyLoader(() => import('./NotFound'))}

  9.  />

Route 中渲染的是 lazyLoader 函数返回的组件,该组件在 mount 之后会去执行 importComponent 函数(既: () =& gt ; import ( './About' ) )动态加载其对于的组件模块(被拆分出来的代码),待加载成功之后渲染该组件。

使用该方式打包出来的代码:

  1. Hash: 02a053d135a5653de985

  2. Version: webpack 3.6 .0

  3. Time: 9399ms

  4.          Asset     Size  Chunks                    Chunk Names

  5. 0.db22.child.js  5.82 kB       0  [emitted]

  6. 1.fcf5.child .js   4.4 kB       1  [emitted]

  7. 2.442d.child.js     3 kB       2  [emitted]

  8.  index.1bbc.js   339 kB       3  [emitted]  [big]  index

三、抽离 Common 资源

8(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 防止其他代码被打包进来。

  1. // webpack.config.js

  2. const path = require('path');

  3. const webpack = require('webpack');

  4. const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

  5. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

  6. const PROJECT_ROOT = path.resolve(__dirname, './');

  7. module.exports = {

  8.  entry: {

  9.    index : './src0/index.js',

  10.    vendor: ['react', 'react-dom', 'react-router-dom', 'immutable']

  11.  },

  12.  output : {

  13.    path: path.resolve(__dirname, 'dist'),

  14.    filename: '[name].[chunkhash:4].js',

  15.    chunkFilename : '[name].[chunkhash:4].child.js',

  16.  },

  17.  module: {

  18.    rules: [

  19.      {

  20.        test: /\.js[x]?$/,

  21.        use: 'babel-loader',

  22.        include : PROJECT_ROOT,

  23.        exclude: /node_modules/

  24.      }

  25.     ]

  26.  },

  27.  plugins: [

  28.    new BundleAnalyzerPlugin(),

  29.     new webpack.DefinePlugin({

  30.      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')

  31.    }),

  32.     new UglifyJSPlugin({

  33.      uglifyOptions: {

  34.        ie8: false,

  35.        output : {

  36.          comments: false,

  37.          beautify: false,

  38.        },

  39.        mangle : {

  40.          keep_fnames: true

  41.        },

  42.        compress : {

  43.          warnings: false,

  44.          drop_console: true

  45.        },

  46.      }

  47.    }),

  48.    new webpack.optimize.CommonsChunkPlugin({

  49.      name : 'vendor',

  50.      minChunks: Infinity,

  51.    }),

  52.   ],

  53.  resolve: {

  54.    extensions: ['.js', '.jsx']

  55.  },

  56. };

运行打包可以看到:

  1. Hash: 34a71fcfd9a24e810c21

  2. Version : webpack 3.6.0

  3. Time: 9618ms

  4.          Asset     Size  Chunks                    Chunk Names

  5. 0.2c65.child.js   5.82 kB       0  [emitted]

  6. 1.6e26.child.js   4.4 kB       1  [emitted]

  7. 2.e4bc.child.js     3 kB       2  [emitted]

  8.  index .4e2f.js  64.2 kB       3  [emitted]         index

  9. vendor.5fd1.js   276 kB       4  [emitted]  [big]  vendor

可以看到 vendor 被单独打包出来了。

当我们改变业务代码时再次打包:

  1. Hash: cd3f1bc16b28ac97e20a

  2. Version: webpack 3.6.0

  3. Time: 9750ms

  4.          Asset     Size  Chunks                    Chunk Names

  5. 0.2c65.child.js  5.82 kB       0  [emitted]

  6. 1.6e26.child. js   4.4 kB       1  [emitted]

  7. 2.e4bc.child.js     3 kB       2  [emitted]

  8.  index.4d45.js  64.2 kB       3  [emitted]         index

  9. vendor .bc85.js   276 kB       4  [emitted]  [big]  vendor

vendor 包同样被打包出来的,然而它的文件 hash 却发生了变化,这显然不符合我们长缓存的需求。

这是因为 webpack 在使用 CommoChunkPlugin 的时候会生成一段 runtime 代码(它主要用来处理代码模块的映射关系),而哪怕没有改变 vendor 里的代码,这个 runtime 仍然是会跟随着打包变化的并且打入 verdor 中,所以 hash 就会开始变化了。解决方案则是把这部分的 runtime 代码也单独抽离出来,修改之前的 CommonsChunkPlugin 为:

  1. // webpack.config.js

  2. ...

  3. new webpack.optimize.CommonsChunkPlugin({

  4.  name: ['vendor', 'runtime'],

  5.  minChunks : Infinity,

  6. }),

  7. ...

执行打包可以看到生成的代码中多了 runtime 文件,同时即使改变业务代码,vendor 的 hash 值也保持不变了。

当然这段 runtime 实际上非常短,我们可以直接 inline 在 html 中,如果使用的是 html - webpack - plugin 插件处理 html,则可以结合 html - webpack - inline - source - plugin 插件自动处理其 inline。

(2)公共资源抽离

在我们打包出来的 js 资源包括不同入口以及子模块的 js 资源包,然而它们之间也会重复载入相同的依赖模块或者代码,因此可以通过 CommonsChunkPlugin 插件将它们共同依赖的一些资源打包成一个公共的 js 资源。

  1. // webpack.config.js

  2. plugins: [

  3.   new BundleAnalyzerPlugin(),

  4.  new webpack.DefinePlugin({

  5.    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')

  6.   }),

  7.  new UglifyJSPlugin({

  8.    uglifyOptions: {

  9.      ie8: false,

  10.      output: {

  11.        comments: false,

  12.        beautify: false,

  13.       },

  14.      mangle: {

  15.        keep_fnames: true

  16.       },

  17.      compress: {

  18.        warnings: false,

  19.        drop_console: true

  20.       },

  21.    }

  22.  }),

  23.   new webpack.optimize.CommonsChunkPlugin({

  24.    name: ['vendor', 'runtime'],

  25.    minChunks: Infinity,

  26.   }),

  27.  new webpack.optimize.CommonsChunkPlugin({

  28.    // ( 公共chunk(commnons chunk) 的名称)

  29.    name: "commons",

  30.     // ( 公共chunk 的文件名)

  31.    filename: "commons.[chunkhash:4].js",

  32.    // (模块必须被 3个 入口chunk 共享)

  33.    minChunks : 3

  34.  })

  35. ],

可以看到这里增加了 commons 的一个打包,当一个资源被三个以及以上 chunk 依赖时,这些资源会被单独抽离打包到 commons .[ chunkhash : 4 ]. js 文件。

执行打包,看到结果如下:

  1. Hash: 2577e42dc5d8b94114c8

  2. Version: webpack 3.6.0

  3. Time : 24009ms

  4.          Asset     Size  Chunks                    Chunk Names

  5. 0.2eee.child.js  90.8 kB       0  [emitted]

  6. 1.cfbc.child.js  







请到「今天看啥」查看全文