webpack性能优化(下)

在webpack性能优化(上)中,我们从 代码分离,Loader, webpack解析(resolve), webpack 外部扩展(Externals) ,Dlls 优化构建速度,等方面分析了优化手段,这篇文章让我们接着来撸。

图片等静态文件 dev prod

通常来说,我们会通过使用file-loader,url-loader等loader来处理项目中的静态文件,如图片字体等文件

//这样最终dist文件中就会生成font文件夹存放字体文件
{
    test: /\.(woff|svg|eot|ttf)\??.*$/,
    loader: "url-loader",
    options: {
        limit: 8192,
        name: "font/[name].[hash:6].[ext]"
    }
}

复制代码

limit属性是在文件大小超出limit的值才会单独打包,否则使用base64 的方式引用通常适用于小图片,这就是我们通常的文件处理方式。

使用base64引入图片的好处是减少http请求数,但相应的问题是base64占用的空间比普通的图片文件大一点。

当然我们还有另外一种方案,具体做法是将项目的中静态文件统一存放在static文件夹下,最后使用 CopyWebpackPlugin将static文件夹拷贝到dist目录下

new CopyWebpackPlugin([
    {
        from: path.resolve(SRC_PATH, 'img'),
        to: 'img'
    }
]),
复制代码

这样做的好处是我们的静态资源不经过webpack的处理,可以提升构建速度,但问题也是很明显的,那就是维护的成本增大并可能出现一些意外的情况,比如:

这样处理的问题是可能开发环境引用路径和打包文件访问图片路径不一致问题,这里可以通过output.publicPath属性来配置解决

output: {
    //打包文件中通过相对路径引用的资源都会被配置的路径所替换
    publicPath: '/assets/'
}

//对于这种结构的项目当然不合适使用这种方法
|- /static
|– /components
|  |– /my-component
|  |  |– index.jsx
|  |  |– index.css
|  |  |– icon.svg
|  |  |– img.png
复制代码

当然从我们实际项目的测试效果来看,我只能说这种处理方式并不算是很优秀,仅供参考。

source map dev

在开发环境中,我们比较关注调试的方便程度,而原始webpack打包后的bundle文件中可能包含来自多个文件的内容,对于程序的报错信息往往简单的指向这个bundle文件:

image.png
而source map是为了帮助我们定位程序出现的错误对应的源代码的位置。使用sourceMap报错信息正确的指向了源码的错误位置。
image.png

//1 使用devtool选项配置,有多个选项可选
module.exports = {
    devtool: 'inline-source-map',
};

//2 使用plugins方式进行更细粒度的配置
module.exports = {
    plugins: [
        new webpack.SourceMapDevToolPlugin({
            filename: '[name].js.map',
            exclude: ['vendor.js']
        })
    ]
};

//在使用uglifyjs-webpack-plugin时 需要开启sourceMap选项

复制代码

devtool文档

UglifyJsPlugin 配合 tree shaking prod

对于js压缩 在webpack <= 3.x的版本中:

//1 使用 -p(production)标记来压缩js
//2 使用内置 plugin(webpack.optimize.UglifyJsPlugin)
//3 使用外部引入plugin(uglifyjs-webpack-plugin)
module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            },
            output: {
                //remove all comments
                comments: false
            }
        }),
    ]
};

复制代码

在webpack4中

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
    //1 设置 mode
    mode: ""production
    //2 minimize
    optimization:{
	minimize: true,
        //3 或者指定其他插件
        minimizer: [
          new UglifyJsPlugin({
            sourceMap: true
          })
        ]
    },
    //3 若需要sourceMap 需要设置 devtool的值
    devtool: 'source-map', 
};

//可选的压缩插件
UglifyJSPlugin, ClosureCompilerPlugin
BabelMinifyWebpackPlugin,

复制代码

此处需要注意。若是在使用了UglifyJSPlugin且开启sourceMap后,需要同时给devtool设置值。同样的若是设置了devtool的值,则UglifyJSPlugin也需要开启sourceMap。否则不会生成.map的源代码对应文件。

在开启js的压缩后 我们的tree shaking登场了,tree shaking是什么?为什么需要使用?

tree shaking是一个术语,用于描述移除js中未引用的代码。 使用它能优化输出。 未开启tree shaking的实例:

//tool.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

//index.js
import { square } from "./tool.js"

//最终输出  在关闭 UglifyJSPlugin插件后测试结果
复制代码

image.png

我们可以看到 cube这个我们并没有引用的模块也被打包进源码了。

使用tree shaking 来优化输出,在package.json中:

//webpack4
//1 将文件标记为无副作用 
{
  "name": "web",
  "version": "1.0.0",
  //若是整个项目都无副作用 直接设置为false
  "sideEffects": false
  //若是部分文件确实有副作用
  "sideEffects": [file_path1, file_path2]
}

//2 开启js压缩 使用上述方法开启即可

//webpack2/3
//1 需要配置 .babelrc modules false
{
  "presets": [
    [
      "env", 
      {
        "modules": false
      }
    ]
  ]
}

//2 开启js压缩

复制代码

「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

对于开启后的压缩代码中,我们搜索"*"号 只得到一个结果,测试成功。

image.png

最后我们简单解释下设置modules false的作用。tree shaking本身是依赖于ES6的静态导入,也就是我们常用的import export。ES6模块化中一个文件能够输出多个模块,而我们可以只导入需要的模块。对比commonjs的动态导入模块化标准,一个文件只有一个输出,因此不难发现,tree shaking在commonjs模块化的系统中是发挥不了作用的。

而modules的意义是启用将ES6模块语法转换为另一种模块类型,默认值'commonjs',将该设置为false即不转化,也就是ES6模块语法,所以在此我们需要将modules设置为false。

modules的取值有 'amd' | 'umd' | 'systemjs' | 'commonjs' | false。 在webpack4中已经可以不用此方法来检测重复模块了

Split CSS prod

一个web项目中css是关键的一环,若没有额外配置css最终会被打包进入js文件中,但熟悉浏览器渲染的开发者应该会清楚,浏览器在渲染页面时会解析DOM树和CSS树,最后将之对应合并呈现渲染好的页面。将css放在js中引入势必会延缓css树的计算。 所以将css从js中分离,打包成单独的css文件,然后和js并行加载是我们项目的一个提升点,这样可以加快界面渲染速度,也可以单独做缓存。

//使用插件 extract-text-webpack-plugin
{
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
        //用于css未被提取(allChunks: false)
        fallback: "style-loader",
        use: 'css-loader'
    })
}

new ExtractTextPlugin({
    filename: 'common.[chunkhash].css',
    allchunk: true
})

复制代码

当然webpack-dev-server是不支持extract-text-webpack-plugin抽离的css热替换的,所以此插件不建议再dev环境中使用,如果非要使用可以考虑css-hot-loader。

清理 /dist 文件夹 prod

webpack将打包的文件放在dist文件夹中,若是使用了hash文件名,则每次文件变动后重新打包生成的文件名都会不同,这会造成dist目录越来越混乱,好的做法是每次打包前先清理dist文件夹:

new CleanWebpackPlugin(pathsToClean, cleanOptions)
复制代码

在内存中编译 dev

webpack-dev-server大家都不陌生,开发环境必备,webpack内部依赖了webpack-hot-middleware,webpack-dev-middleware两个插件。

webpack-dev-middleware提供了在内存中编译功能,它在文件更改后自动编译文件并保存在内存中,具体表现为,刷新浏览器即可看到我们的更改。

webpack-hot-middleware提供了服务端推送功能,通常和webpack-dev-middleware配合使用,当文件更改并自动编译完成后,服务端通过SSE(服务端发送事件)将更改信息推送到客户端,客户端会接收到一个json文件,其中包含了更改了的文件的一些信息,客户端会根据这些信息主动向服务端获取最新的文件。

若无文件更改webpack-hot-middleware也会在一定时间间隔后遍历内存文件检测是否更改,然后通过事件流的方式向客户端发送消息。

webpack-dev-middleware和webpack-hot-middleware都是express的标准插件

我相信各位项目中这两个功能都是已经开启的我就不再具体说他们的配置了,这里我们主要说下在node服务端怎么使用这两个插件达到热更新的目的。

我们以koa为例,如何在koa中开启热更新调试我们的项目呢?

//新建 app.js作为koa服务端入口 app.js

import Koa from "koa";
import views from "koa-views";
import webpack from "webpack";
import webpack_config from "../webpack/webpack.config.js";
import { devMiddleware, hotMiddleware } from 'koa-webpack-middleware'

var app = new Koa()
var compiler = webpack(webpack_config)

app.use(views("./template", {map: {html: "ejs"}}));

app.use(devMiddleware(compiler,{
    publicPath:"/"
}));

app.use(hotMiddleware(compiler))

复制代码

koa-webpack-middleware 将express的中间件(webpack-dev-middleware和webpack-hot-middleware)进行封装,将我们koa中间件的next方法传递到express的第三个参数中进行封装。

最简单的配置如上。但这种配置会有一个问题就是刷新404的问题。

hotMiddleware会在匹配到项目跟路由时直接返回内存中的html文件给客户端。但是其他的路由如react的路由时,它不会去匹配,最终会返回一个404

//会返回template/index.html 但这时是空的
//也就是没有导入js的html
await ctx.render("index");
复制代码

解决,当用户访问时在webpack编译输出的最后阶段获取到文件信息,取出获取到的html文件写入template下的index.html文件,最后返回它,具体操作如下:

compiler.plugin("emit",(comilation,callback) => {
    const assets = comilation.assets;
    let file, data;
    Object.keys(assets).forEach(key => {
        if(key.match(/\.html$/)){
            file = path.resolve(__dirname,"./template/index.html");
            data = assets[key].source();
            fs.writeFileSync(file,data);
        }
    });

    callback();
})
复制代码

当然上述方法略显笨重,且需要理解的东西较多,不太推荐,这里有一个插件能解决上述问题 connect-history-api-fallback,大家自己学习下即可。

我的博客地址

猜你喜欢

转载自juejin.im/post/5bfb5c9c5188250c1021337c