webpack优化入门详解

3,696 阅读2分钟

以下测试中,均在webpack版本3.5以上,如有不对的地方请指出,谢谢。

新特性优化

Scope Hoisting-作用域提升,将模块都放到一个闭包函数中,通过减少闭包函数数量从而加快JS的执行速度。 基本配置如下:

plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
]    

配置后,自定义的两个模块函数被放在一个必包函数体中,打包结果如下:

配置前,自定义的两个模块函数分别在不同的必包函数体中,通过行数就能看出来,打包结果如下:

配置项优化

externals配置优化

设置externals配置项分离不需要打包的库文件,然后在模版文件中使用script引入即可,配置代码片段如下:

externals: {
  'jquery': 'jquery'
},

alias配置优化

  • 可以选择对应的需要打包文件的大小规格
  • 通过定义全局路径,减少webpack编译过程中的搜索硬盘时间

基本配置代码片段如下:

resolve: {
  extensions: ['.js', '.json'],
  alias: {
    'jquery': 'jquery/dist/jquery.slim.min.js',
    '@': resolve('src'),
  }
},

插件使用优化

内链css减少请求

webpack插件配置片段代码如下:

plugins: [
  new StyleExtHtmlWebpackPlugin({
    minify: true
  })
]

最终打包可以将css文件直接以内链的形式插入网页中,进而可以减少网页请求。

preload插件使用

webpack插件配置片段代码如下:

plugins: [
  new PreloadWebpackPlugin({
    rel: 'preload',
    as: 'script',
    include: 'all'
  })
]

最终打包生成的页面在head上会生成preload的link标签,代码片段如下:

<link rel="preload" as="script" href="app.8898b6c9e3b39f7a1c9d.js">
<link rel="preload" as="script" href="common.6ecc97ec2b5dceebbd5e800322c2a3c0.css">
<link rel="preload" as="script" href="vendor.dcd374ee43fd57d2365b.js">
<link rel="preload" as="script" href="manifest.f3e58576762e216d8867.js">

更多配置参考这里详细更多

缓存优化与分析

css打包优化

contenthash

使用webpack打包css用的是extract-text-webpack-plugin插件,由于hash是webpack的module identifier计算的,不变内容的情况下每次打包也会产生不同的hash,因此选用chunkhash,它是根据文件内容计算的,因此比较符合实际使用,基本配置片段代码如下:

module: {
  rules: [{
    test: /\.(less|scss|css)$/,
    use: ExtractTextPlugin.extract({
      fallback: "style-loader",
      use: [
        {
          loader:"css-loader",
          options:{
            minimize: true //css压缩
          }
        }, 
        {
          loader:"less-loader",
          options:{
            minimize: true //css压缩
          }
        }, 
        {
          loader:"sass-loader",
          options:{
            minimize: true //css压缩
          }
        }]
    })
  }]
},
plugins: [
  new ExtractTextPlugin({
    filename: 'common.[chunkhash].css',
    allChunks: true
  }),
]

单纯改变css文件

// a.less
.ac {
 .bc {
   font-size: 12px;
 }
 .cc {
   font-weight: 700;
   border: 1px solid #ccc;
 }
}

module文件中的a.js引入了a.less文件

// module/a.js
require('../style/a.less');

多次打包发现hash值未改变,不但依赖的js文件hash未改变,就连css文件的hash也未改变,最终使用webpack打包如下:

这显然不是想要的结果,希望改变css文件内容,js文件的hash不会改变,只有相应的css文件的hash值改变,因此再ExtractTextPlugin插件中应该使用contenthash,这也是官方特别注明的,直到再次看到文档才发现。

设置contenthash之后,再次打包发现只有css文件hash变了,并且改变js代码依然不影响css文件的打包,最终打包结果如下:

js打包优化

HashedModuleIdsPlugin

webpack插件基本配置片段代码如下:

entry: {
  app: './src/app.js',
  vendor: ['lodash']
},
plugins: [
    new CleanWebpackPlugin(['cdist']),
    new HtmlWebpackPlugin({
      template: './src/index.template.html', //html模板路径
      filename: 'wq.html', //生成的html存放路径,相对于path
      favicon: './src/favicon.ico', //favicon路径,通过webpack引入同时可以生成hash值
      inject: 'body', //js插入的位置,true/'head'/'body'/false
      // chunks: ['app', 'vendor'],
      //hash: true ,//为静态资源生成hash值
      minify: { //压缩HTML文件
        removeComments: true, //移除HTML中的注释
        collapseWhitespace: false //删除空白符与换行符
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
    new ExtractTextPlugin({
      filename: 'common.[contenthash].css',
      allChunks: true
    }),
    new webpack.optimize.UglifyJsPlugin({
      beautify: false,
      comments: false,
      compress: {
        warnings: false,
        drop_console: true,
        collapse_vars: true,
        reduce_vars: true,
      }
    })
  ]

项目入口文件为app.js

import _ from 'lodash';
import a from './module/a';
// import b from './module/b';
require("babel-polyfill")
require('./style/lib.css')

function fn () {
  // let aa = _.clone({key:4})
  // aa.key = 2;
  return aa
}

fn()

第一次打包生成的文件如下:

修改入口文件app.js代码如下:

import _ from 'lodash';
import a from './module/a';
import b from './module/b';
require("babel-polyfill")
require('./style/lib.css')

function fn () {
  let aa = _.clone({key:4})
  aa.key = 2;
  return aa
}

fn()

再次使用webpack打包,最后显示如下:

从打包结果来看,不但入口文件app.js哈希值变了,第三方库vendor.js的哈希值也变了,这当然不是我想要的,因为我更本没有改变第三方库文件。为了解决这个问题可以引入HashedModuleIdsPlugin这个配置项,这样就可以解决以上遇到的问题。

将插件配置做如下修改,代码片段如下:

plugins: [
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
  ]

在经过以上两个步骤,再次使用webpack打包,结果如下:

webpack3中tree-shaking分析

tree-shaking其实是webpack2中就有的功能特性,但是会因模块函数定义的形式,会有失效的概率,因此,我想用webpack3来测试下,看看是否官网有优化这部分特性。

tree-shaking必须有UglifyJsPlugin这个配置项才能生效,否则统一不生效。

常规定义方法

module/a.js代码如下:

require('../style/a.less');
function a() {
  return 'aaaaaa'
}
export {a};

module/b.js代码如下:

require('../style/b.scss');
function b() {
  return 'bbbbbbb'
}
export {b};

入口文件代码如下:

// app.js
import _ from 'lodash';
import { a } from './module/a';
import {b} from './module/b';
require("babel-polyfill")
require('./style/lib.css')
function wq() {
  return a()
}
wq()

最后使用webpack打包,在压缩的文件中发现bbbbbbb被删除了,因此可以得出一个结论,webpack会动态判断引入的包是否被使用从而再次精简打包文件大小。

原型定义方法

这次我改写a.js和b.js的形式,再其原型链上定义方法,基本代码如下:

// module/a.js
require('../style/a.less');
function a() {
  return 'aaaaaa'
}
a.prototype.fn = () => {
  return 'aaaaaa'
}
export {a};
// module/b.js
require('../style/b.scss');
function b() {
  return 'bbbbbbb'
}
b.prototype.fn = () => {
  return 'bbbbbbb'
}
export {b};

入口文件代码如下:

// app.js
import _ from 'lodash';
import { a } from './module/a';
import {b} from './module/b';
require("babel-polyfill")
require('./style/lib.css')

function wq() {
  return a.prototype.fn()
}

wq()

最后使用webpack打包,在压缩的文件中发现bbbbbbb没有被删除,只是调用函数的方法不一样,导致tree-shaking并没有生效。

class类编写模式

我再次使用es6中类的概念改写a、b的定义方法,基本代码如下:

// module/a.js
require('../style/a.less');
class a {
  fn() {
    return 'aaaaaaa'
  }
}
export {a};
// module/b.js
require('../style/b.scss');
class b {
  fn() {
    return 'bbbbbb'
  }
}
export {b};

最后使用webpack打包,在压缩的文件中发现bbbbbbb依然没有被删除,tree-shaking并没有生效。

let定义形式

最后我使用最简单的定义形式改写,基本代码如下:

// module/a.js
require('../style/a.less');
let a = 'aaaaaaa'

export {a};
// module/b.js
require('../style/b.scss');
let b = 'bbbbbbb'

export {b};

最后使用webpack打包,在压缩的文件中发现bbbbbbb被删除,tree-shaking生效。

分析

原型定义写法不会被删除很好理解,就是这些原型方法,因为原型方法可能会将要被调用,是一个未知情况,如果直接删除,那么一旦其他模块会动态调用原型方法,那就会造成代码报错,这样是不合理的。

class类的写法似乎不会产生副作用,类中的函数似乎也不是将来可能被调用,再来看最后webpack打包生成的代码,片段如下:

/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__ = __webpack_require__("Zrlr");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__);
var b = function () {
  function b() {
    __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default()(this, b);
  }

  __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default()(b, [{
    key: 'fn',
    value: function fn() {
      return 'bbbbbb';
    }
  }]);

  return b;
}();

首先b是一个自运行函数,并且最终还调用了__webpack_require__("Zrlr")这个模块,因此就类似prototype,会产生一点有副作用的函数,所以不能直接删除。

最后贴上完整的webpack配置项,这里并没有加上vue/react的相关配置,如需要可以自己安装相应插件,通过配合babel-loader进行编译打包,.babelrc配置入门详解

const webpack = require('webpack'); //to access built-in plugins
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin')
const PreloadWebpackPlugin = require('preload-webpack-plugin')


const config = {
  entry: {
    app: './src/app.js',
    vendor: ['lodash']
  },
  output: {
    path: path.resolve(__dirname, 'cdist'),
    filename: '[name].[chunkhash].js'
  },
  externals: {
    'jquery': 'jquery'
  },
  // resolve: {
  //   extensions: ['.js', '.json'],
  //   alias: {
  //     'jquery': 'jquery/dist/jquery.slim.min.js',
  //     '@': resolve('src'),
  //   }
  // },
  module: {
    rules: [{
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(less|scss|css)$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: [
            {
              loader:"css-loader",
              options:{
                minimize: true //css压缩
              }
            }, 
            {
              loader:"less-loader",
              options:{
                minimize: true //css压缩
              }
            }, 
            {
              loader:"sass-loader",
              options:{
                minimize: true //css压缩
              }
            }]
        })
      }
    ]
  },
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
    new CleanWebpackPlugin(['cdist']),
    new HtmlWebpackPlugin({
      template: './src/index.template.html', //html模板路径
      filename: 'wq.html', //生成的html存放路径,相对于path
      favicon: './src/favicon.ico', //favicon路径,通过webpack引入同时可以生成hash值
      inject: 'body', //js插入的位置,true/'head'/'body'/false
      // chunks: ['app', 'vendor'],
      //hash: true ,//为静态资源生成hash值
      minify: { //压缩HTML文件
        removeComments: true, //移除HTML中的注释
        collapseWhitespace: false //删除空白符与换行符
      }
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      as: 'script',
      include: 'all'
    }),
    // 解决第三方打包文件hash值不变,最大化缓存
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
    new ExtractTextPlugin({
      filename: 'common.[contenthash].css',
      allChunks: true
    }),
    new webpack.optimize.UglifyJsPlugin({
      beautify: false,
      comments: false,
      compress: {
        warnings: false,
        drop_console: true,
        collapse_vars: true,
        reduce_vars: true,
      }
    }),
    new StyleExtHtmlWebpackPlugin({
      minify: true
    })
  ]
};

module.exports = config;