使用 webpack 3 构建高性能的应用程序

10年服务1亿前端开发工程师

几天前,我有幸升级了我们项目的 Webpack 构建,因为我们要努力改进应用程序的性能。 最大限度的压缩 Webpack 构建包,无疑给我们带来了显著的改进。

在这里,我将尝试分享一些参考指南,可以帮助你更好地了解 Webpack 配置,并为你正在进行的项目找到一种最佳设置。

注意: 我仅在下面的示例中显示了部分代码,但请注意,插件配置的顺序对于其中一些可能至关重要。本文的底部包含了全部要点。

迁移 webpack 版本

如果您仍在考虑是否升级你的 webpack 1 ,我会告诉你 – 肯定升级。v1 → v2 可能有点麻烦,但 v2 → v3 在98%的情况下可以平滑升级(根据Webpack的团队统计)。

单靠迁移将提高捆绑包的性能并显著降低文件大小。我们在 v2 中获得了 tree shaking 功能,而在 v3 中获得了 作用域提升(scope hoisting)。第一个来自 v2 的内置功能,但是 作用域提升(scope hoisting)需要在 v3 中使用 ModuleConcatenationPlugin 来启用。

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

代码拆分(Code Splitting) 和 缓存(Caching)

我们可以将我们的应用程序和第三方库代码 拆分 成单独的文件,而不是得到一个包含全部的巨大的 bundle.js 。

我们可以通过为我们的应用程序代码定义一个 入口点(entry point) ,然后使用 CommonsChunkPluginnode_modules 的所有内容捆绑到 第三方库(vendor) 包中。

我们还将通过 chunkhash 添加缓存清除,以便所有输出文件具有不同的哈希。每个文件哈希在每个构建中都保持不变,除非它的文件内容发生了变化。

entry: {
  app: path.resolve(sourcePath, 'index.js')
},
output: {
  path: path.join(__dirname, 'dist'),
  filename: '[name].[chunkhash].js',
  publicPath: '/'
},
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: 'vendor.[chunkhash].js',
    minChunks (module) {
      return module.context &&
             module.context.indexOf('node_modules') >= 0;
    }
  })
]

动态导入(Dynamic import) 和 延迟加载(Lazy Loading) ?

这种类型的优化仍然是值得商榷。有很多因素需要考虑,而且几种不同的方式来实现这一点,但最终还是取决于项目的特性。

Webpack 2 在 动态导入 方面有所改进。虽然传统方式是使用 require.ensure ,最新版本遵循 TC39标准提案 ,并使用 import() 语法。

另一方面,通过使用 bundle-loader ,react-router 4 改进了组件的 延迟加载

我没有时间深入测试,但我所尝试的东西没有太大的影响。你甚至可以说,他们降低了我一直在努力的项目性能。

压缩(Minification)

压缩 Webpack 的输出是非常重要的,以减少它的文件大小。

html-webpack-plugin.html 文件很好的编译工具。我们可以使用模板引擎( .hbs | .ejs )作为索引文件,它将被编译为 index.html

const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
  new HtmlWebpackPlugin({
    template: path.join(__dirname, 'index.ejs'),
    path: buildPath,
    excludeChunks: ['base'],
    filename: 'index.html',
    minify: {
      collapseWhitespace: true,
      collapseInlineTagWhitespace: true,
      removeComments: true,
      removeRedundantAttributes: true
    }
  })
]

压缩 .js 输出文件首先使用内置的 UglifyJsPlugin 。下面是许多教程都建议的 Uglify 压缩配置,是生产构建的最佳选择之一。

还有一个选项,使用标识符而不是模块名称来压缩输出。启用这个功能只需简单的调用 HashedModuleIdsPlugin 即可 (开发时推荐使用 NamedModulesPlugin ) 。

plugins: [
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: false,
      screw_ie8: true,
      conditionals: true,
      unused: true,
      comparisons: true,
      sequences: true,
      dead_code: true,
      evaluate: true,
      if_return: true,
      join_vars: true
    },
    output: {
      comments: false
    }
  }),
  new webpack.HashedModuleIdsPlugin()
]

我们可以通过使用 生产版本 NODE_ENV 设置进一步减小 React 库文件的尺寸,这将消除所有面向开发的警告和开销。(注:webpack官网文档说明请查看 Node 环境变量

plugins: [
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
  })
]

内联关键(critical) CSS

注:

关键(critical) CSS,翻译过来为 关键 CSS, 一般是指页面首屏最小的 CSS 集合,建议将这部分样式写到 <head> 标签内,这样可以使页面首屏尽可能快的被渲染。

当用户连接速度慢时,我们希望页面首屏尽可能快的渲染,所以一般的做法是确定项目中 关键(critical) CSS 和 可视部分,并将这些CSS直接内联在页面上(例如一般布局,页眉和页脚,字体)。这样我们就可以提供感知上更快的加载速度。

我们可以简单地将 SCSS 模块划分为 base.scssstyle.scss,在那里,其中 base 将包含所有这些 关键(critical) 部分。

第一步是从 webpack 的入口点(entry points) 提取 并且压缩所有单独的 CSS 文件。之后,我们将 关键(critical) CSS 部分 内联 到编译后的 index.html 中的 </head><head> 标签中。

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin');
plugins: [
  new ExtractTextPlugin({
    filename: '[name].[contenthash].css',
    allChunks: true
  }),
  new StyleExtHtmlWebpackPlugin({
    minify: true
  })
]

Source maps (源映射)

通过选择源映射样式,我们可以进一步减小构建包的大小。webpack 开发工具选项使你能够设置合适的样式。

有多种风格选项,但通常大多数在线指南或教程都建议使用 source-map 选项进行开发,并且在生产构建中使用 cheap-module-source-map

devtool: 'cheap-module-source-map'

异步(async) 和 延迟(defer)

注:

例如,<script async src="my.js"> 或者 </script><script async src="my.js"> 。相关说明可以查看 MDN 的文档

将其中一个模式设置到 </script><script> 标签中,将会修改其加载顺序,从而可以促使页面更快的加载,因为你避免渲染阻塞的请求。所以建议从可以稍后执行的代码中识别并分离出需要立即执行的代码。

使用 script-ext-html-webpack-plugin ,我们可以为 script 设置不同的部署选项进行编译。

const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
plugins: [
  new ScriptExtHtmlWebpackPlugin({
    defaultAttribute: 'defer'
  })
]

警告:异步(async) 和 延迟(defer) 属性在执行时间上有本质上的不同。两种类型的加载将与HTML解析并行执行。不同的是,异步(async) 脚本将在其加载完成后立即执行,而 延迟(defer) 脚本将等待 HTML 解析完成后,并按加载顺序执行。

DNS 预读取(Dns-prefetch) 和 预加载(preload)

如果您需要使用第三方代码,可以考虑使用某些域名的 dns-prefetch 链接。这将确保浏览器在稍后请求资源之前进行 DNS 查找。

<link rel="dns-prefetch" href="https://www.<example_domain>.com">

预加载(preload) 将提前启动优先级高,以及将来将被使用资源的非渲染阻塞获取。要在编译时添加这些特性,我们可以使用 preload-webpack-plugin

const PreloadWebpackPlugin = require('preload-webpack-plugin');
plugins: [
  new PreloadWebpackPlugin({
    rel: 'preload',
    as: 'script',
    include: 'all',
    fileBlacklist: [/\.(css|map)$/, /base?.+/]
  })
]

启用 gzip 压缩

gzip 压缩构建文件将大大减小它们的文件大小。为了使其工作,我们还需要对你的 Web 服务器配置进行修改(例如 Apache | Nginx )。如果浏览器支持 gzip ,那么它将会被接收到。

const CompressionPlugin = require('compression-webpack-plugin');
plugins: [
  new CompressionPlugin({
  asset: '[path].gz[query]',
  algorithm: 'gzip',
  test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$|\.svg?.+$/,
  threshold: 10240,
  minRatio: 0.8
  })
]

Service worker

如果需要的话,可以考虑使用 service worker 来缓存你的构建文件(全部或部分)。有一些非常好的插件可以开箱即用,例如 offline-pluginsw-precache-webpack-plugin ,或者你也可以编写自己的 service worker 。

这是配置文件最终的样子:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PreloadWebpackPlugin = require('preload-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const autoprefixer = require('autoprefixer');

const staticSourcePath = path.join(__dirname, 'static');
const sourcePath = path.join(__dirname, 'src');
const buildPath = path.join(__dirname, 'dist');

module.exports = {
    devtool: 'cheap-module-source-map',
    entry: {
        base: path.resolve(staticSourcePath, 'src/sass/base.scss'),
        app: path.resolve(sourcePath, 'index.js')
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].[chunkhash].js',
        publicPath: '/'
    },
    resolve: {
        extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js', '.jsx'],
        modules: [
            sourcePath,
            path.resolve(__dirname, 'node_modules')
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
        }),
        new webpack.optimize.ModuleConcatenationPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: 'vendor.[chunkhash].js',
            minChunks (module) {
                return module.context && module.context.indexOf('node_modules') >= 0;
            }
        }),
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false,
                screw_ie8: true,
                conditionals: true,
                unused: true,
                comparisons: true,
                sequences: true,
                dead_code: true,
                evaluate: true,
                if_return: true,
                join_vars: true
            },
            output: {
                comments: false
            }
        }),
        new webpack.LoaderOptionsPlugin({
            options: {
                postcss: [
                    autoprefixer({
                        browsers: [
                            'last 3 version',
                            'ie >= 10'
                        ]
                    })
                ],
                context: staticSourcePath
            }
        }),
        new webpack.HashedModuleIdsPlugin(),
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'index.ejs'),
            path: buildPath,
            excludeChunks: ['base'],
            filename: 'index.html',
            minify: {
                collapseWhitespace: true,
                collapseInlineTagWhitespace: true,
                removeComments: true,
                removeRedundantAttributes: true
            }
        }),
        new PreloadWebpackPlugin({
            rel: 'preload',
            as: 'script',
            include: 'all',
            fileBlacklist: [/\.(css|map)$/, /base?.+/]
        }),
        new ScriptExtHtmlWebpackPlugin({
            defaultAttribute: 'defer'
        }),
        new ExtractTextPlugin({
            filename: '[name].[contenthash].css',
            allChunks: true
        }),
        new StyleExtHtmlWebpackPlugin({
            minify: true
        }),
        new CompressionPlugin({
            asset: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$|\.svg?.+$/,
            threshold: 10240,
            minRatio: 0.8
        })
    ],
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: [
                    'babel-loader'
                ],
                include: sourcePath
            },
            {
                test: /\.scss$/,
                exclude: /node_modules/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [
                        { loader: 'css-loader', options: { minimize: true } },
                        'postcss-loader',
                        'sass-loader'
                    ]
                })
            },
            {
                test: /\.(eot?.+|svg?.+|ttf?.+|otf?.+|woff?.+|woff2?.+)$/,
                use: 'file-loader?name=assets/[name]-[hash].[ext]'
            },
            {
                test: /\.(png|gif|jpg|svg)$/,
                use: [
                    'url-loader?limit=20480&name=assets/[name]-[hash].[ext]'
                ],
                include: staticSourcePath
            }
        ]
    }
};

结果

我将在开发和生产环境中添加应用程序加载时间的截图,并且网络限制为良好的3G网络。

请注意,由于加载的字体和图像,存在一些开销,但是这个优化主题对于本文来说太大了。

第一个案例是开发构建包。正如下面所示,js的文件是很大,因为我们把所有东西都捆绑在一起,没有进行优化。

app.83852f151d1eccbe4a08.js         2.63 MB
app.83852f151d1eccbe4a08.js.map     3.08 MB      
index.html                          1.31 kB

性能、时间及各项数据请查看下图:

当我们应用构建优化时,我们最终会得到拆分后的应用程序代码文件和第三方库代码文件,main css 文件,并且 base css 直接注入到 index.html 中。

我们压缩的文件大小超过89%,并设法改善了加载时间超过74%!

app.6c682e3a87517dd36425.js.gz                  21.9 kB
app.a5a5d15f6b07d45545a0794a327ab904.css.gz     30.9 kB
vendor.68f62e37ce5bcaf5df30.js.gz               220 kB
index.html                                      13.7 kB

性能、时间及各项数据请查看下图:

结论

绝对值得升级和优化你的 webpack 配置,特别是如果你想要构建高性能的应用程序的话。您将看到版本升级带来的改进(我们提升了大约4到5秒的加载时间,v1 – > v3)。

最后,我希望本文能帮助你更好地了解如何使用 webpack 改进构建和应用程序性能的基本准则。

原文链接:https://medium.com/netscape/webpack-3-react-production-build-tips-d20507dba99a

赞(0) 打赏
未经允许不得转载:WEB前端开发 » 使用 webpack 3 构建高性能的应用程序

评论 5

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #-49

    稍微格式化一下代码吧,看着有点费劲

    阿逮逮1年前 (2017-07-21)回复
    • 代码格式化了啊,估计是你脚本没加载完

      1年前 (2017-07-21)回复
      • 估计他说的是缩进…

        青椒肉丝1年前 (2017-07-25)回复
        • 估计他是手机上看的

          1年前 (2017-07-25)回复
  2. #-48

    我有个问题,为啥提取公共模块后js变大了,这是怎么回事

    chjiyun9个月前 (04-20)回复

前端开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏