IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    让你的 Webpack 起飞 —— 网易考拉会员 Webpack4 性能优化实战

    雨浣潇湘发表于 2019-07-28 03:54:02
    love 0

    最近看到了美团的前端团队的一篇文章,文中提到前端发布仅需10秒,默默的看了一下我们自己的发布时间。。。

    先定一个小目标,争取把 Webpack 的打包时间优化到10秒以内吧。

    先看一下现在打包一次需要的时间,73013ms,下面开始一步一步见证奇迹:

    73013
    73013

    0. 模块分析

    有很多工具提供了可视化的分析,如Webpack-bundle-analyzer、webpack-chart、 webpack-analyse。
    以Webpack-bundle-analyzer为例,它提供了一个下图所示的图表,展示了引入的所有模块的大小、路径等信息,可以针对性的做出优化。

    Webpack-bundle-analyzer
    Webpack-bundle-analyzer

    使用上也很简单:

    1
    2
    // 安装:
    npm install webpack-bundle-analyzer --save-dev
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js 配置
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

    plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server',
      analyzerHost: '127.0.0.1',
      analyzerPort: 8888,
      reportFilename: 'report.html',
      defaultSizes: 'parsed',
      openAnalyzer: true,
      generateStatsFile: false,
      statsFilename: 'stats.json',
      logLevel: 'info'
    })
    ]

    运行webpack命令,会自动在浏览器中打开http://127.0.0.1:8888/页面,展示可视化图表。

    1. 升级到 Weback4.x

    Webpack4 带来了极大的性能提升,按照开发者博客中的说法,构建速度最多甚至有高达98%的提升。

    升级过程中遇到了一些网上的“Webpack4升级指南”等文章中没有列出的问题,在此分享一下:

    1.1 升级 Vue-loader

    Vue-loader 目前最新版本为 v15.2.6,使用方式有了很大不同。
    现在,我们需要引入一个新的插件 VueLoaderPlugin ,具体使用方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    const VueLoaderPlugin = require('vue-loader/lib/plugin')

    module.exports = {
    plugins: [
    new VueLoaderPlugin()
    ]
    }

    同时,在 v15版本的 Vue-loader 中,不再需要单独为 .vue 组件中的模板、CSS等内容单独配置 loader,可以共用普通文件的配置,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // webpack.config.js

    module: {
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader'
    }, {
        // 它会应用到普通的 `.js` 文件
        // 以及 `.vue` 文件中的 `<script>` 块
        test: /\.js$/,
        loader: 'babel-loader'
    }, {
        // 它会应用到普通的 `.css` 文件
        // 以及 `.vue` 文件中的 `<style>` 块
        test: /\.css$/,
        use: [
         'vue-style-loader',
         'css-loader'
        ]
    }]
    }

    1.2 升级 Vue-router

    在Vue-router v13.0.0版本中对模块导入做了更新,需要加入 default 配置,如下所示:

    1
    2
    3
    const Foo = () => import('./Foo.vue')
    // 需要改为
    const Foo = () => import('./Foo.vue').then(m => m.default)

    同理:
    1
    2
    3
    const Foo = require('./Foo.vue')
    // 需要改为
    const Foo = require('./Foo.vue').default

    详情可以参考https://github.com/vuejs/vue-loader/releases/tag/v13.0.0

    1.3 Chunk 的命名
    如果使用 webpackchunkname 魔法注释来命名,需要注意 .babelrc 中 comment 必须为true

    1.4 提取 CSS 文件

    在 Webpack4 环境下 extract-text-webpack-plugin 需要安装 @next 版本,我们这里直接使用了 mini-css-extract-plugin 来代替。

    同时,在 mode 为 development 时,NamedChunksPlugin 和 NamedModulesPlugin 会默认开启,不需要再显式指定。

    1.5 本地 mock 处理

    2.x版本的 Vue-CLI 启动了一个 express 服务来处理本地数据的 mock,我们尝试做了一些简化,在 webpack-dev-server 的 before 方法中,使用 webpack-api-mocker 插件拦截了请求,读取本地的 mock 数据(JSON文件)返回。其中,mock/index.js 是通过服务启动过程中遍历本地数据文件生成的。

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.dev.conf.js
    const apiMocker = require('webpack-api-mocker');
    require('./mock-generator.js')();
    devServer: {
    before(app) {
    apiMocker(app, path.resolve('./mock/index.js'));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // mock/index.js
    const fs = require('fs');
    function fromJSONFile(filepath) {
    return (req, res) => {
    const data = fs.readFileSync('mock' + filepath).toString();
    const json = JSON.parse(data);
    return res.json(json);
    };
    };
    const proxy = {
    'GET /aaa/bbb': fromJSONFile('/aaa/bbb.json'),
    'GET /aaa/ccc': fromJSONFile('/aaa/ccc.json')
    };
    module.exports = proxy;

    升级完成之后,打包时间直接减少了半分钟,达到了44.534秒,离小目标还有很大距离,我们继续。

    44534
    44534

    2. 路由处理(异步加载)

    在前面的打包完成的图片中,我们可以看到生成了大量的文件,统计了一下,体积总计高达22.07M,文件近60个。
    权衡了诸如加载时间等方面之后,我们决定采用按照一级路由来打包的方式。
    具体实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    Vue.use(Router);
    import Vue from 'vue';
    import Router from 'vue-router';
    export default new Router({
    routes: [{
    path: '/a',
    component: () =>
    import ( /* webpackChunkName: 'a' */ '@/pages/a'),
    name: 'a'
    }, {
    path: '/b',
    component: () => import ( /* webpackChunkName: 'b' */ '@/pages/b'),
    name: 'b',
    children: [{
    path: 'b/m',
    component: require('@/pages/b/m').default,
    name: 'm',
    children: [{
    path: 'b/m/p',
    component: require('@/pages/b/m/p').default,
    name: 'p'
    }, {
    path: 'b/m/q',
    component: require('@/pages/b/m/q').default,
    name: 'q'
    },
    ]
    }]
    }]

    经过上面的优化,我们将生成的js文件数量减少到了8个,大小减小到了5M.
    来看一下打包时间:21.959秒!距离小目标越来越近了。

    21959
    21959

    3. HappyPack/thread-loader

    HappyPack 可以将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展为多进程的模式,从而加速代码构建。使用方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const HappyPack = require('happypack');
    const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

    module: {
    loaders: [{
    test: /\.less$/,
    loader: ExtractTextPlugin.extract(
    'style', path.resolve(__dirname, './node_modules', 'happypack/loader') + '?id=less'
    )
    }]
    },
    plugins: [
    new HappyPack({
    id: 'less',
    loaders: ['css!less'],
    threadPool: happyThreadPool,
    cache: true,
    verbose: true
    })
    ]

    经过测试,在我们的项目中,对 js 和 ts 文件使用 happypack 收益最大。

    此处需要注意的是,vue-loader 不支持 happypack,可以使用 thread-loader 来进行加速,同样是新建一个进程来执行 loader 操作,使用方式也很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
     module: {
    rules: [{
    test: /\.vue$/,
    use: [
    'thread-loader',
    'vue-loader'
    ]
    }]
    }

    但是在我们的项目中,经过测试,thread-loader 对于打包速度几乎没有影响,是因为它本身的额外开销导致,建议只在极高性能消耗的场景下使用。

    完成之后,测试一下打包时间:15.101秒。

    14814
    14814

    4. 缓存loader的执行结果(cacheDirectory/cache-loader)

    我们可以对loader做如下配置来开启缓存:

    1
    loader: 'babel-loader?cacheDirectory=true'

    或者我们也可以使用 cache-loader :

    1
    2
    3
    4
    5
    6
    7
    rules: [{
    test: /\.vue$/,
    use: [
    'cache-loader',
    'vue-loader'
    ]
    }]

    加入缓存之后,再次测试打包时间:13.915秒。
    13915
    13915

    5. 模块进一步细分(splitChunks)

    在 Webpack4 中移除了我们此前常用的 CommonsChunkPlugin 插件,取而代之的是 splitChunks 。
    splitChunks 的默认配置已经足够我们日常使用,没有特殊需求可以不必特意处理。
    我们此处的配置如下(生产环境):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    optimization: {
    splitChunks: {
    cacheGroups: {
    commons: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendors',
    chunks: 'all'
    },
    // styles: {
    // name: 'index',
    // test: /.stylus|css$/,
    // chunks: 'all',
    // enforce: true
    // }
    }
    }
    }

    其中,commons 部分的作用是分离出 node_modules 中引入的模块,styles 部分则是合并 CSS 文件。
    经过测试,在我们的项目中,styles 部分使构建时间增加了大约2秒,因此我们放弃了这部分操作。

    6. 使用DllPlugin拆分模块

    开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用DllPlugin和DllReferencePlugin单独构建它们。
    具体使用如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    module.exports = {
    entry: {
    vendor: [
    'axios',
    'vue-i18n',
    'vue-router',
    'vuex'
    ]
    },
    output: {
    path: path.resolve(__dirname, '../static/'),
    filename: '[name].dll.js',
    library: '[name]_library'
    },
    plugins: [
    new webpack.DllPlugin({
    path: path.join(__dirname, 'build', '[name]-manifest.json'),
    name: '[name]_library'
    })
    ]
    }

    执行webpack命令,build目录下即可生成 dll.js 文件和对应的 manifest 文件,使用 DLLReferencePlugin 引入:
    1
    2
    3
    4
    5
    6
    plugins: [
    new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./build/vendor-manifest.json')
    })
    ]

    由于我们的项目中原本已经通过这种方式打包了大部分第三方库,所以这里对打包速度的提升不大,仅仅提升2秒,来到了11.509秒。
    11509
    11509

    7. 精简不必要的模块

    在我们的项目中,引入了一些如 moment、lodash 等重型库,然而他们提供的绝大部分功能都是我们不需要的,权衡之后,我们移除了他们,自己实现了部分功能或使用了更小体积的库代替。

    移除了这些库之后,我们的打包时间来到了8.921秒!小目标达成了~

    8921
    8921

    但是这距离10秒发布还不够,我们需要争取压缩出更多时间留给发布系统。还能不能继续提升呢?答案是肯定的。

    8. 优化模块查找路径

    Node.js的模块的载入及缓存机制如下:

    载入内置模块
    载入文件模块
    载入文件目录模块
    载入node_modules里的模块
    自动缓存已载入模块
    如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。

    我们可以对搜索过程进行一些优化,比如可以像下面这样指定路径:

    1
    2
    3
    4
    5
    6
    7
    8
    exclude: /node_modules/, // 排除不处理的目录
    include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
    resolve: {
    modules: [path.resolve(__dirname, 'node_modules')], // 指定node_modules的位置
    alias: {
    'api': resolve('src/api') // 创建别名
    }
    }

    我们再来看一下时间:7.66秒!

    7660
    7660

    到这里,我们的这次优化基本完成了,其实还有很多可以优化的空间,比如升级一颗 i9 处理器~
    这里也只是列举出了一些常见的收益较大的优化方式,希望能对大家有一点帮助,也欢迎有兴趣的同学一起交流。



沪ICP备19023445号-2号
友情链接