前端必知:vue性能优化——webpack 配置优化

1,730 阅读10分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

当我们自己搭建项目框架的时候,会用到webpack构建我们的项目,在用webpack构建项目的时候,过长的打包编译时间和庞大冗余的代码会让我们感到头疼。所以优化webpack性能成为了不可或缺的一部分。下面我们一起来探讨webpack性能优化细节。

1.Webpack 对图片进行压缩

在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片。

(1)安装image-webpack-loader

npm install image-webpack-loader --save-dev

(2)在 webpack.base.conf.js  中进行配置

{
    test: /.(png|jpe?g|gif|svg)(?.*)?$/,
    use:[ 
        { 
            loader: 'url-loader', 
                options: {  limit: 10000,  name: utils.assetsPath('img/[name].[hash:7.[ext]')  } 
         }, 
         {  loader: 'image-webpack-loader',   options: { bypassOnDebug: true } 
         } 
  ]}

2.剔除无效代码 tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。

(1)JS的tree shaking主要通过uglifyjs插件来完成 安装:

npm install --save-dev uglifyjs-webpack-plugin

配置:

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

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
 },
  plugins: [
   new UglifyJSPlugin()
  ]
};

(2)CSS的tree shaking主要通过purify CSS来实现的

安装:

npm i -D purifycss-webpack purify-css

配置:

const path = require('path');
const glob = require('glob');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PurifyCSSPlugin = require('purifycss-webpack');

module.exports = {
  entry: {...},
  output: {...},
  module: {
    rules: [
      {
        test: /.css$/,
        loader: ExtractTextPlugin.extract({
          fallbackLoader: 'style-loader',
          loader: 'css-loader'
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin('[name].[contenthash].css'),
    // Make sure this is after ExtractTextPlugin!
    new PurifyCSSPlugin({
      // Give paths to parse for rules. These should be absolute!
      paths: glob.sync(path.join(__dirname, 'app/*.html')),
    })
  ]
};

如果要设置多路径,则需要将glob换成glob-all

const glob = require('glob-all');
paths: glob.sync([
  path.join(__dirname, '.php'),
  path.join(__dirname, 'partials/.php')
])

3.打包公共代码

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用。这会带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

new webpack.optimize.CommonsChunkPlugin(options)

配置项

{
  name: string, // or
  names: string[],
  // common chunk 的名称

  filename: string,
  // common chunk 的文件名模板。可以包含与 `output.filename` 相同的占位符

  minChunks: number|Infinity|function(module, count) -> boolean,
  // 在传入公共chunk(commons chunk) 之前所需要包含的最少数量的 chunks 。
  // 数量必须大于等于2,或者少于等于 chunks的数量

  chunks: string[],
  // 通过 chunk name 去选择 chunks 的来源。chunk 必须是 公共chunk 的子模块。

  children: boolean,
  // 如果设置为 `true`,所有公共chunk 的子模块都会被选择

  deepChildren: boolean,
  // If `true` all descendants of the commons chunk are selected

  async: boolean|string,
  // 如果设置为 `true`,一个异步的公共chunk 会作为 `options.name` 的子模块,和 `options.chunks` 的兄弟模块被创建。

  minSize: number,
  // 在 公共chunk 被创建立之前,所有公共模块 (common module) 的最少大小。
}

提取公共代码:

new webpack.optimize.CommonsChunkPlugin({
  name: "commons",
  // ( 公共chunk(commnons chunk) 的名称)

  filename: "commons.js",
  // ( 公共chunk 的文件名)
})

明确第三方库chunk:

entry: {
  vendor: ["jquery", "other-lib"],
  app: "./entry"
},
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: "vendor",
    minChunks: Infinity,
  })
]

将公共模块打包进父 chunk:

new webpack.optimize.CommonsChunkPlugin({
  children: true,
})

额外的异步公共chunk:

new webpack.optimize.CommonsChunkPlugin({
  name: "app",
  // or
  names: ["app", "subPageA"]
  children: true,
  async: true,
  minChunks: 3,
})

webpack 4 将移除 CommonsChunkPlugin, 取而代之的是两个新的配置项 optimization.splitChunks 和 optimization.runtimeChunk

通过设置 optimization.splitChunks.chunks: "all" 来启动默认的代码分割配置项

当满足如下条件时,webpack 会自动打包 chunks:

当前模块是公共模块(多处引用)或者模块来自 node_modules
当前模块大小大于 30kb
如果此模块是按需加载,并行请求的最大数量小于等于 5
如果此模块在初始页面加载,并行请求的最大数量小于等于 3

4.长缓存优化chunkhash

将hash替换为chunkhash,这样当chunk不变时,缓存依然有效。

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'Caching'
    })
  ],
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  }
};

提取模板:

CommonsChunkPlugin 可以用于将模块分离到单独的文件中。还能够在每次修改后的构建结果中,将 webpack 的样板(boilerplate)和 manifest 提取出来。通过指定 entry 配置中未用到的名称,此插件会自动将我们需要的内容提取到单独的包中。

const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'Caching'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    })
  ],
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  }
};

将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用客户端的长效缓存机制,可以通过命中缓存来消除请求,并减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。这可以通过使用新的 entry(入口) 起点,以及再额外配置一个 CommonsChunkPlugin 实例的组合方式来实现。

var path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: [
      'lodash'
    ]
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'Caching'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    })
  ],
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  }
};

[注意]CommonsChunkPlugin 的 'vendor' 实例,必须在 'manifest' 实例之前引入

5.忽略依赖库的解析

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/, //不去解析jquery | lodash 中的依赖库
  }
};

如果js中引入jquery, webpack会去解析jq中是否有依赖库,配置noParse后打包时候忽略解析配置的库,提高打包效率。

6.解析时指定和排除查找目录

module:{
		rules:[
			{
				test:/.js$/,
				exclude:/node_modules/, // 解析不包含的目录,两者写其一即可
				include:path.resolve('src'), // 即系包含的目录,两者写其一即可
				use:{
					loader:'babel-loader',
					options:{
						presets:[
							'@babel/preset-env',
							'@babel/preset-react'
...

exclude排除目录不进行解析。

7.配置source-map

source-map就是源码映射,主要是为了方便代码调试,因为我们打包上线后的代码会被压缩等处理,导致所有代码都被压缩成了一行,如果代码中出现错误,那么浏览器只会提示出错位置在第一行,这样我们无法真正知道出错地方在源码中的具体位置。webpack提供了一个devtool属性来配置源码映射。

devtool常见的有4种配置:
① source-map: 这种模式的特点是会产生一个.map文件,文件里面保留了打包后的文件与原始文件之间的映射关系,能够通过map文件逆向解析出源码内容,所以出错了能够提示到具体的行和列,打包输出文件中会指向生成的.map文件,告诉js引擎源码在哪里,由于源码与.map文件分离,所以需要浏览器发送请求去获取.map文件,常用于生产环境,如:

// 打包后输出的bundle文件末尾会包含以下内容
//# sourceMappingURL=index.js.map

source-map解析到的是真正的源码,也就是说是loader处理前的源码,所以source-map是最慢的同时提示信息也是最全的

② eval: 这种模式打包速度最快,但是不会生成.map文件,会使用eval将模块包裹,在末尾加入sourceURL,常用于开发环境,如:

//# sourceURL=webpack:///./src/index.js

报错的时候只能定位到具体是哪个文件路径无法知道其在源码中的行列信息

③ eval-source-map: 每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap(即base64编码形式内嵌到eval语句末尾), 但是不会生成.map文件,可以减少网络请求,浏览器中点击报错文件链接,光标可以定位到源码中出错位置的行和列,但是打包文件会非常大

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/index.js

本质上说,eval-source-map和source-map其实是一样的,只不过是将map文件通过base64编码的形式内置到了打包输出文件中。所以其报错信息也是全(包含行和列)并且准确(loader转换前)的。

④ cheap-source-map: 加上 cheap,相对于source-map而言,就只会提示到第几行报错,少了列信息提示,可以提高打包性能,但是仍然会产生.map文件,并且其解析到的是经过loader转换后的源码

⑤ cheap-eval-source-map: 加上 cheap,就只会提示到第几行报错,相对于eval-source-map,少了列信息提示,并且其解析到的是经过loader转换后的源码

所以加上cheap之后,报错信息就变得不准确了,少了列信息,解析到的源码是经过loader处理之后的

⑥ cheap-module-source-map: 和cheap-source-map相比,加上了module,报错的时候定位到的就是loader转换前的源码位置,并且也会产生.map文件,用于生产环境

⑦ cheap-module-eval-source-map: 常用于开发环境,使用 cheap 模式可以大幅提高 souremap 生成的效率,加上module可以定位到源码经过loader转换前的位置,eval提高打包构建速度,并且不会产生.map文件减少网络请求。

凡是带eval的模式都不能用于生产环境,因为纯eval只能定位报错文件路径,无法知道具体信息;eval-source-map其不会产生.map文件,会导致打包后的文件变得非常大。通常我们并不关心列信息,所以都会使用cheap模式,但是我们需要查看loader转换前的源码,以便精准找到错误的位置,所以使用cheap的时候要加上module。开发环境通常用cheap-module-eval-source-map,生产环境通常用cheap-module-source-map

8.开启模块热更新

模块热更新可以做到在不刷新网页的情况下,更新修改的模块,只编译变化的模块,而不用全部模块重新打包,大大提高开发效率,在未开启热更新的情况下,每次修改了模块,都会重新打包。要开启模块热更新,那么只需要在devServer配置中添加hot:true即可。当然仅仅开启模块热更新是不够的,我们需要做一些类似监听的操作,当监听的模块发生变化的时候,重新加载该模块并执行,如:

module.exports = {
    devServer: {
        hot: true // 开启热更新
    }
}

----------


import foo from "./foo";
console.log(foo);
if (module.hot) {
    module.hot.accept("./foo", () => { // 监听到foo模块发生变化的时候
        const foo =  require("./foo"); // 重新引入该模块并执行
        console.log(foo);
    });
}

9.使用IgnorePlugin

可以忽略某个模块中某些目录中的模块引用,比如在引入某个模块的时候,该模块会引入大量的语言包,而我们不会用到那么多语言包,如果都打包进项目中,那么就会影响打包速度和最终包的大小,然后再引入需要使用的语言包即可,如:
项目根目录下有一个time包,其中有一个lang包,lang包中包含了各种语言输出对应时间的js文件,time
包下的index.js会引入lang包下所有的js文件,那么当我们引入time模块的时候,就会将lang包下的所有js文件都打包进去,添加如下配置:

const webpack = require("webpack");
module.exports = {
    plugins: [
        new webpack.IgnorePlugin(/lang/, /time/)
    ]
}

引入time模块的时候,如果time模块中引入了其中的lang模块中的内容,那么就忽略掉,即不引入lang模块中的内容,需要注意的是,这/time/只是匹配文件夹和time模块的具体目录位置无关,即只要是引入了目录名为time中的内容就会生效