提升webpack打包速度

时间:2024-10-10 08:06:07

webpack打包文件体积过大,怎么提升速度?

借助webpack visualizer可视化插件,来看构建的情况。
这个问题要具体情况具体分析,看看打包文件有哪些块头比较大,哪些不常改变,最好列一个list,分类优化。下面提供8种方法,仅供参考。

一、区分配置文件,去除不必要的插件。
前端开发环境通常分为两种:开发环境和生产环境,我们可以创建两个配置文件来区分开发环境和生产环境。
webpack.config.dev.js,用于开发环境,需要日志输出,sourcemap,热模块替换,错误报告等。
webpack.config.prod.js,用于生产环境,需要做代码压缩,对文件名进行hash处理等。

//安装webpack-merge
npm install --save-dev webpack-merge
//webpack.config.common.js

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

module.exports = {
  entry: {
    app: './src/index.js'
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'Production'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
//webpack.config.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.config.common.js');

module.exports = merge(common, {
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  }
});
//webpack.config.prod.js

const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.config.common.js');

module.exports = merge(common, {
  devtool: 'source-map',
  plugins: [
    new UglifyJSPlugin({
      sourceMap: true
    })
  ]
});

在webpack.config.common.js中,我们设置了entry和output配置,并且在其中引入这两个环境公用的全部插件。在webpack.config.dev.js中,我们添加了推荐的devtool(强大的 source map)和简单的devServer配置。在webpack.config.prod.js中,我们引入了UglifyJSPlugin。

修改package.json文件

"scripts": {
  "dev": "webpack-dev-server --open --config webpack.config.dev.js",
  "prod": "webpack --config webpack.config.prod.js"
}

二、合理使用CommonsChunkPlugin插件,提取公用代码。

该方法适用于开发环境和生产环境,具体用法在下面有介绍。

三、通过externals配置来引用外部文件的方式,提取第三方库。

{
  externals: {
    'react': 'React'
  }
}
<script src="//cdn.com/vue.min.js"></script>
<script src="/build/bundle.js"></script>

该方法适用于生产环境。

四、利用DllPlugin和DllReferencePlugin预编译资源模块,提取第三方库。

const vendors = [
  'vue','vue-router',
  // ...其它库
];

module.exports = {
  output: {
    path: 'build',
    filename: '[name].js',
    library: '[name]'
  },
  entry: {
    "lib": vendors
  },
  plugins: [
    new webpack.DllPlugin({
      path: 'manifest.json',
      name: '[name]',
      context: __dirname
    })
  ]
};

该方法适用于开发环境和生产环境。

五、压缩代码

使用tree shaking,即先用es6语法import,export,再引入删除引用代码的压缩工具UglifyJsPlugin。

new webpack.optimize.UglifyJsPlugin({
    // 最紧凑的输出
    beautify: false,
    // 删除所有的注释
    comments: false,
    compress: {
        //在UglifyJs删除没有用到的代码时不输出警告
        warnings: false,
        //用于压缩的时候去除log
        drop_debugger: true,
        drop_console: true,
        //内嵌定义了但是只用到一次的变量
        collapse_vars: true,
        //提取出现多次但是没有定义成变量去引用的静态值
        reduce_vars: true
    }
})

加入了这个插件之后,编译的速度会明显变慢,所以一般只在生产环境启用。如果服务器端可以开启gzip压缩,优化的效果更明显。
另外,可以通过webpack-parallel-uglify-plugin来提升压缩速度,原理是采用多核并行压缩的方式提升速度。

let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
let os = require('os');

new ParallelUglifyPlugin({
   workerCount: os.cpus().length,
   cacheDir: '.cache/',
   uglifyJS: {
        output: {
            comments: false
        },
        compress: {
            warnings: false
        }
    }
})

该方法适用于生产环境。

六、happyPack多核编译资源(多线程介入

var HappyPack = require('happypack');
const os = require('os');
const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
//启动线程池

let happyPackConfig = {
    plugins: [
        new HappyPack({
            id:'jsx',
            threadPool: HappyThreadPool,
            cache: true,
            loaders: ['babel-loader']
        })
    ]
}

{
    test: /.jsx?$/,
    exclude: /(node_modules|bower_components)/,
    use: {
        loader: 'HappyPack/loader?id=jsx'
    }
}

七、代码分割
分离代码,实现缓存资源和并行加载资源。
对于大型app而言,将所有代码都放在一个文件中是不高效的,尤其是一些代码块只要某些情况下才需要加载。webpack有一个特性,就是能够对项目进行代码分割。代码分割不只是提取通用代码放入可分享的块,更重要的是可以将代码分割成按需加载的块。代码分割可选择,可定义分割点,下面介绍几种分割代码的方法。
1、entry
当项目多入口点时,必须改写默认的output.filename选项,否则每个入口点都会写入相同的文件。
A、多个入口文件,打包在一起;

'use strict';
const webpack = require("webpack");
module.exports = {
  context: __dirname + "/src",
  entry: {
    app: ["./file1.js", "./file2.js", "./file3.js"]
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js"
  }
};

打包结果:所有文件会按数组顺序一起打包到dist/app.bundle.js文件中。

B、多个入口文件,分开打包。

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    file1: "./file1.js",
    file2: "./file2.js",
    file3: "./file3.js"
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

打包结果:dist/file1.bundle.js、dist/file2.bundle.js、dist/file3.bundle.js。

2、CommonsChunkPlugin插件

防止重复,移除重复的依赖模块。

const path = require('path');
const webpack = require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    file1: "./file1.js",
    file2: "./file2.js",
    file3: "./file3.js"
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common' // 指定公共bundle的名称。
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

3、动态import
ES2015模块加载规范定义了import()方法来运行时动态地加载ES2015模块,webpack2将import()作为分割点并将被请求的模块放到一个单独的chunk中,import()接收模块名作为参数,并返回一个Promise。

//webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
// 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);
});

在注释中使用了webpackChunkName,这样做会导致我们的bundle被命名为lodash.bundle.js,而不是[id].bundle.js 。

4、commonjs的require.ensure
require.ensure(dependencies, callback);
require.ensure方法确保回调时,dependencies里的每个依赖都会被同步的引入,require作为参数传递到回调函数中。require.ensure只加载模块,并不执行。

require.ensure(["module-a", "module-b"], function(require) {
  var a = require("module-a");
  // ...
});

5、AMD的require
reuqire(dependices, callback);
调用时,所有依赖都被加载,callback函数中能得到依赖暴露出来的对象。AMD的require加载并执行模块,在webpack中,模块由左向右执行,允许省略回调函数。

require(["module-a", "module-b"], function (a, b) {// ...})

6、按需加载

像d3,echarts,lodash,antd,比较大,没必要全部加载。

let d3 = Object.assign({},
    require("d3-selection"),
    require("d3-transition"),
    require("d3-shape")
);

//为d3.event加上get方法,获取d3Selection.event
Object.defineProperty(d3,"event", {get: function() { return d3Selection.event; }});
// 引入echarts主模块

var echarts = require('echarts/lib/echarts');

//引入柱状图
require('echarts/lib/component/bar');

//引入提示框和标题组件
require('echarts/lib/component/tooltip');
require('echarts/lib/component/title');

//基于准备好的dom,初始化echarts

var myChart = echarts.init(document.getElementById('main'));

//绘制图表
myChart.setOption({
    title: { text:'走向社会'},
    tooltip:{},
    xAxis:{ data:['2013年','2014年','2015年','2016年','2017年','2018年']},
    yAxis: {},
    series: [{
        name: '技术积累',
        type: 'bar',
        data: [5,8,11,15,19,23]
    }]
})
## 安装lodash相关插件
cnpm i -S lodash-webpack-plugin babel-plugin-lodash
//配置webpack.config.js

var LoashModuleReplacementPlugin = require('lodash-webpack-plugin');

{
    ...,
    plugins: [
        ...,
        new LodashModuleReplacementPlugin({
            'collections': true,
            'shorthands': true
        })
    ]
}
//在.babelrc中增加lodash配置
{
    "plugins": [..., "lodash"]
}
//引用一次,即可调用任意函数,而不会完全打包

import  _  from 'lodash';
_.add(1,2); //打包时,只会引入add函数

八、缓存不常修改的库文件
不要在开发环境下使用[chunkhash],因为这会增加编译时间。将开发和生产模式的配置分开,并在开发模式中使用[name].js的文件名,在生产模式中使用[name].[chunkhash].js文件名。

1、output.filename

var path = require("path");
var webpack = require("webpack");
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");
var WebpackChunkHash = require("webpack-chunk-hash");

module.exports = {
  entry: {
    vendor: "./src/vendor.js",
    main: "./src/index.js"
  },
  output: {
    path: path.join(__dirname, "build"),
    filename: "[name].[chunkhash:8].js",
    chunkFilename: "[name].[chunkhash:8].js"
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ["vendor", "manifest"],
      minChunks: Infinity
    }),
    new webpack.HashedModuleIdsPlugin(),
    new WebpackChunkHash(),
    new ChunkManifestPlugin({
      filename: "chunk-manifest.json",
      manifestVariable: "webpackManifest"
    })
  ]
};