头图

简析 webpack 的打包优化

前言:关于前端优化始终是无法跟构建工具相分离,下面由我从日常的前端的视角从文件的提取、合并、拆分等几个方面来说一下 webpack 是如何进行打包配置的。
写在前面:

前端是怎么做到性能优化的? 带着这个问题 我们从几个方面出发:

  • 减少 HTTP 请求
  • 静态资源使用 CDN
  • 将 CSS 放在文件头部,JavaScript 文件放在底部
  • 使用字体图标 iconfont 代替图片图标
  • 善用缓存,不重复加载相同的资源
  • 使用服务端渲染
  • ...

详情可以看:前端性能优化 24 条建议;大佬总结的很细致,但是如果从 webpack 出发的话 我们对以上的建议可以做到几条?

答:都可以


项目准备

以 react 为例

webpack: 4.31.0
webpack-cli : 3.3.2

解释一下 为什么用的是 webpack4x,目前组内大部分都是用的 webpack4 进行构建,后面也会单独出一份关于 webpack5 的教程。

目录结构

image.png

如上图所示,当前的项目放置一些静态的文件即可。

package.json

项目开始前的准备插件

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --open --config webpack.dev.js", // 本地运动
    "build": "webpack --config webpack.prod.js"  // 打包优化
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.13.10",
    "@babel/preset-env": "^7.13.10",
    "@babel/preset-react": "^7.13.13",
    "babel-loader": "^8.2.2",
    "html-webpack-plugin": "^3.2.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  },
  "dependencies": {
    "webpack-dev-server": "^3.11.2"
  }
}
.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

减少 http 请求

合并图片资源

   npm i url-loader -D
webpack.prod.js
 module: {
    rules: [
      ...
      {
        test: /.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 102400, // 100k
            },
          },
        ],
      },
    ],
  },

场景:对于项目中有几个的小图标可以使用 url-loader 进行优化处理,它默认会把文件转为 DataURL,如果文件小于 limit,那么 url-loader 会调用file-loader进行处理。当前,图标过多推荐使用iconfont进行加载。

参考: url-loader


减少文件的搜索范围

webpack.prod.js
 module: {
    rules: [
      ...
      {
        test: /.js$/,
        use: [
          {
            loader: 'babel-loader',
            exclude: /node_modules/,  // 不需要被解析的模块
           // include: path.resolve('src')  // 不需要被解析的模块
          },
        ],
      },
    ],
  },

静态资源使用 CDN

配置publicPath

webpack.prod.js
output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js', // 多个入口的情况下 不知道对应的名称、可以用占位符来指定[name]
    publicPath: 'https://cdn.example.com/assets/', // 配置CDN地址
  },
mode: 'production', // 生产环境

将 CSS 放在文件头部,JavaScript 文件放在底部

配置H5的rem适配方案为例:

npm i px2rem-loader raw-loader@0.5.1 lib-flexible  -D

新建 src/meta.html 添加公共的 mate 标签

<!-- 公共的 meta信息-->
<meta charset="UTF-8" />
<meta name="viewport"
  content="width=device-width,initial-scale=1,maximum-scale=1, minimum-scale=1,user-scalable=no,viewport-fit=cover" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />

src/meta.html 配置:

<!DOCTYPE html>
<html lang="en">

<head>
  ${require('raw-loader!./meta.html')}
  <title>Document</title>
  <script>${ require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') }</script>
</head>

<body>
  <div id="root"></div>
</body>

</html>

场景:使用raw-loader可以动态的配置模板的占位符,达到渲染的位置。需要注意的是raw-loader@0.5.1比较稳定,其他版本有些许问题。

参考: raw-loader


抽离资源文件

npm i html-webpack-externals-plugin -D
webpack.prod.js
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
...
plugins:[
     new HtmlWebpackExternalsPlugin({
      // 提取公共资源
      externals: [
        {
          module: 'react',
          entry: 'https://unpkg.com/react@16/umd/react.production.min.js',
          global: 'React',  // 全局注入
        },
        {
          module: 'react-dom',
          entry: 'https://unpkg.com/react-dom@16/umd/react-dom.production.min.js',
          global: 'ReactDOM', // 全局注入
        },
        {
          module: 'google-roboto',
          entry: {
            path: '//at.alicdn.com/t/font_460072_qm96unh8hja.css',
            type: 'css',
          },
        },
      ],
      files: ['index.html']
    })
]

优化结果:

优化前:
image.png

优化后:
image.png

场景:对于项目中存在很多的插件或者 UI 组件库等、可以在 HTML 插入外链的 CDN 方式进行打包优化,如果有加载顺序渲染的限制也可以使用raw-loader进行打包设置。

参考: html-webpack-externals-plugin


分离公共方法

使用 webpack 内置的SplitChunksPlugin,它的强大之处是可以在项目中分离公共方法的引入次数。

例子: 在 src/ 创建common/index.js

export function common() {
  return '我是公共的JavaScript';
}

分别在:src/index/index.jssrc/search/index.js

import { common } from '../common';
...

let result = common();
console.log(result);
webpack.prod.js
plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/index.html'),
      chunks: ['commons', 'index'],  // 引入当前的名称 commons
    }),
  ],
...
 optimization: {
    splitChunks: {
      minSize: 0, // 引入的模块的大小,设置为0 有引入就会打包成模块
      cacheGroups: {
        commons: {
          minChunks: 1, // 最少引入的次数
          name: 'commons',  // 命名chunks_name
          chunks: 'all',
        },
      },
    },
  },

场景: 可以根据项目引入的次数进行公共方法 chunk 的抽离,不用在每次文件构建中反复构建。

参考: SplitChunksPlugin


善用缓存构建

yarn add  hard-source-webpack-plugin -D
// or
npm i  hard-source-webpack-plugin -D
webpack.prod.js
...
plugins: [
     new HardSourceWebpackPlugin(),
  ],

第一次构建:

image.png

第二次构建:
image.png

场景:当项目在本地构建的时候需要的依赖较多,可以增加为模块提供中间缓存的方式进行构建,构建的速度可以达到80%左右。 详细的配置也可以参考文档

参考: hard-source-webpack-plugin


文件指纹

yarn add mini-css-extract-plugin -D
// or
npm i  mini-css-extract-plugin -D
Hash :和整个项目的构建相关,只要项目文件有修改,整个项目构建的`hash`值就会随之更改
Chunkhash:和`webpack`打包的`chunk`有关,不同的`entry`会申城不同的`chunkhash`值
Contenthash : 根据文件内容来定义`hash`,文件内容不变,则`contenthash`不变
webpack.prod.js
"use strict";

const path = require("path");
const MiniCssExtractplugin = require("mini-css-extract-plugin"); // 提取css单独一个文件

module.exports = {
  entry: {
    // 入口文件可以用对象的形式来写
    index: "./src/index.js",
    search: "./src/search.js",
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name]_[chunkhash:8].js", // chunkhash 8位的长度
  },
  mode: "production",
  module: {
    rules: [
      {
        test: /.js$/,
        use: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /.css$/, // 配置css的后缀名
        exclude: /node_modules/,
        use: [MiniCssExtractplugin.loader, "css-loader"], //tips:执行的顺序是右到左的
      },
      {
        test: /.less$/, // 配置less的后缀名
        exclude: /node_modules/,
        use: [MiniCssExtractplugin.loader, "css-loader", "less-loader"], //tips:执行的顺序是右到左的
      },
      {
        test: /.(png|jpg|gif|jpeg)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name]_[hash:8].[ext]",
            },
          },
        ],
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name]_[hash:8].[ext]",
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractplugin({
      filename: "[name]_[contenthash:8].css",
    }),
  ],
};

场景:

用作版本管理时,如果一个项目需要发布,只需要发布修改过的文件指纹;对于没有修改过的文件,用户在访问的时候,依旧可以使用浏览器缓存好的,无需二次加载,加速页面访问。

参考:

MiniCssExtractPlugin


代码压缩

  1. uglifyjs-webpack-plugin // js 压缩 [内置]
  2. optimize-css-assets-webpack-plugin // css 压缩
  3. html-webpack-plugin // html 压缩
yarn add html-webpack-plugin optimize-css-assets-webpack-plugin cssnano -D
// or
npm i html-webpack-plugin optimize-css-assets-webpack-plugin cssnano  -D
webpack.prod.js
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins:[
    ...

    new OptimizeCssAssetsWebpackPlugin({
      assetNameRegExp: /.css$/g, // 匹配的正则的名称后缀、跟loader配置一致
      cssProcessor: require('cssnano'), // 用于最小化的css处理器,默认是cssnano
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/search.html'),
      filename: 'search.html',
      chunks: ['search'],
      /**
       * inject : true || 'head' || 'body' || false
       * body : 所有javascript资源将被放置在body元素的底部。
       * head : 把脚本放置在head元素中.
       * true : script标签位于html文件的 body 底部 [默认]
       * false: 不插入生成的 js 文件,只是单纯的生成一个 html 文件
       *  */
      inject: true,
      minify: {
        collapseWhitespace: true, // 清理html中的空格、换行符。 默认值:false
        minifyCSS: true, // 压缩html内的样式。默认值:false
        minifyJS: true, // 压缩html内的js。 默认值:false
        removeComments: false, // 清理html中的注释。 默认值:false
      },
    }),
]

参考:

optimize-css-assets-webpack-plugin
cssnano
html-webpack-plugin

Tips:_关于详细的html-webpack-pluginminify可以详细的参考_


写在最后

在目前的前端的性能优化中,构建工具必不可少,怎么做才能使当前的项目更快、性能更好是前端业界的一个老生畅谈的问题、只有熟练的掌握构建工具的配置才能在性能渲染独领风骚。

参考文献

https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://juejin.cn/post/684490...

其他

插播一条招聘信息,LeapFE 招聘前端工程师

如果你对 用户体验、交互操作流程及用户需求 "有一些" 追求如果你对 web 、小程序 、Electron 技术 "有一些" 认识如果你 很擅长前端新技术的学习和分享 👏 欢迎加入好未来,欢迎加入 LeapFE 一起做一些有意思的事情

image.png

字节内推,发送简历至 zhengqingxin.dancing@bytedanc...

1.1k 声望
2.3k 粉丝
0 条评论
推荐阅读
2022,每个 Leaper 都会有好未来
我是郑庆鑫,前好未来励步事业部的前端负责人,LeapFE是我们团队的简称,曾几何时,我们团队的共同愿景是成为业界知名的前端团队。而今年由于政策对公司毁灭性打击,这个愿景要搁置了,但对于我们绝大多数人,已...

LeapFE31阅读 4.5k评论 6

封面图
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木149阅读 12.3k评论 10

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青54阅读 7.8k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 5.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.1k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木43阅读 7.3k评论 6

字节内推,发送简历至 zhengqingxin.dancing@bytedanc...

1.1k 声望
2.3k 粉丝
宣传栏