Webpack: 构建优化

概述

前面章节我们已经详细探讨 Webpack 中如何借助若干工具分析构建性能,以及如何使用缓存与多进程能力提升构建性能的基本方法与实现原理,这两种方法都能通过简单的配置,极大提升大型项目的编译效率。

除此之外,还可以通过一些普适、细碎的最佳实践,减少编译范围、编译步骤提升性能,包括:

  • 使用最新版本 Webpack、Node;
  • 配置 resolve 控制资源搜索范围;
  • 针对 npm 包设置 module.noParse 跳过编译步骤;
  • 等等。

下面我们一一展开,解释每条最佳实践以及背后的逻辑。

使用最新版本

始终使用最新 Webpack 版本,这算的上是性价比最高的优化手段之一了!从 Webpack V3,到 V4,再到最新的 V5 版本,虽然构建功能在不断叠加增强,但性能反而不断得到优化提升,这得益于 Webpack 开发团队始终重视构建性能,在各个大版本之间不厌其烦地重构核心实现,例如:

  • V3 到 V4 重写 Chunk 依赖逻辑,将原来的父子树状关系调整为 ChunkGroup 表达的有序图关系,提升代码分包效率;
  • V4 到 V5 引入 cache 功能,支持将模块、模块关系图、产物等核心要素持久化缓存到硬盘,减少重复工作。

其次,新版本通常还会引入更多性能工具,例如 Webpack5 的 cache(持久化缓存)、lazyCompilation(按需编译,下面展开介绍) 等。因此,开发者应该保持时刻更新 Webpack 以及 Node、NPM or Yarn 等基础环境,尽量使用最新稳定版本完成构建工作。

使用 lazyCompilation

Webpack 5.17.0 之后引入实验特性 lazyCompilation,用于实现 entry 或异步引用模块的按需编译,这是一个非常实用的新特性!

试想一个场景,你的项目中有一个入口(entry)文件及若干按路由划分的异步模块,Webpack 启动后会立即将这些入口与异步模块全部一次性构建好 —— 即使页面启动后实际上只是访问了其中一两个异步模块, 这些花在异步模块构建的时间着实是一种浪费!lazyCompilation 的出现正是为了解决这一问题。用法很简单:

// webpack.config.js
module.exports = {// ...experiments: {lazyCompilation: true,},
};

启动 lazyCompilation 后,代码中通过异步引用语句如 import('./xxx') 导入的模块(以及未被访问到的 entry)都不会被立即编译,而是直到页面正式请求该模块资源(例如切换到该路由)时才开始构建,效果与 Vite 相似,能够极大提升冷启速度。

此外,lazyCompilation 支持如下参数:

  • backend: 设置后端服务信息,一般保持默认值即可;
  • entries:设置是否对 entry 启动按需编译特性;
  • imports:设置是否对异步模块启动按需编译特性;
  • test:支持正则表达式,用于声明对那些异步模块启动按需编译特性。

不过,lazyCompilation 还处于实验阶段,无法保证稳定性,接口形态也可能发生变更,建议只在开发环境使用。

约束 Loader 执行范围

Loader 组件用于将各式文件资源转换为可被 Webpack 理解、构建的标准 JavaScript 代码,正是这一特性支撑起 Webpack 强大的资源处理能力。不过,Loader 在执行内容转换的过程中可能需要比较密集的 CPU 运算,如 babel-loadereslint-loadervue-loader 等,需要反复执行代码到 AST,AST 到代码的转换。

因此开发者可以根据实际场景,使用 module.rules.includemodule.rules.exclude 等配置项,限定 Loader 的执行范围 —— 通常可以排除 node_module 文件夹,如:

// webpack.config.js
module.exports = {// ...module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: ["babel-loader", "eslint-loader"],},],},
};

配置 exclude: /node_modules/ 属性后,Webpack 在处理 node_modules 中的 js 文件时会直接跳过这个 rule 项,不会为这些文件执行 Loader 逻辑。

此外,excludeinclude 还支持类似 MongoDB 参数风格的值,也就是通过 and/not/or 属性配置组合过滤逻辑,如:

const path = require("path");
module.exports = {// ...module: {rules: [{test: /\.js$/,exclude: {and: [/node_modules/],not: [/node_modules\/lodash/]},use: ["babel-loader", "eslint-loader"]}],}
};
  • 提示:详情可查阅 官网。

上述配置的逻辑是:过滤 node_modules 文件夹中除 lodash 外的所有文件。使用这种能力,我们可以适当将部分需要转译处理的 NPM 包(例如代码中包含 ES6 语法)纳入 Loader 处理范围中。

使用 noParse 跳过文件编译

有不少 NPM 库已经提前做好打包处理(文件合并、Polyfill、ESM 转 CJS 等),不需要二次编译就可以直接放在浏览器上运行,例如:

  • Vue2 的 node_modules/vue/dist/vue.runtime.esm.js 文件;
  • React 的 node_modules/react/umd/react.production.min.js 文件;
  • Lodash 的 node_modules/lodash/lodash.js 文件。

对我们来说,这些资源文件都是独立、内聚的代码片段,没必要重复做代码解析、依赖分析、转译等操作,此时可以使用 module.noParse 配置项跳过这些资源,例如:

// webpack.config.js
module.exports = {//...module: {noParse: /lodash|react/,},
};
  • 提示: noParse 支持正则、函数、字符串、字符串数组等参数形式,具体可查阅官网。

配置后,所有匹配该正则的文件都会跳过前置的构建、分析动作,直接将内容合并进 Chunk,从而提升构建速度。不过,使用 noParse 时需要注意:

  • 由于跳过了前置的 AST 分析动作,构建过程无法发现文件中可能存在的语法错误,需要到运行(或 Terser 做压缩)时才能发现问题,所以必须确保 noParse 的文件内容正确性;
  • 由于跳过了依赖分析的过程,所以文件中,建议不要包含 import/export/require/define 等模块导入导出语句 —— 换句话说,noParse 文件不能存在对其它文件的依赖,除非运行环境支持这种模块化方案;
  • 由于跳过了内容分析过程,Webpack 无法标记该文件的导出值,也就无法实现 Tree-shaking。

综上,建议在使用 noParse 配置 NPM 库前,先检查 NPM 库默认导出的资源满足要求,例如 React@18 默认定义的导出文件是 index.js

// react package.json
{"name": "react",// ..."main": "index.js"
}

node_module/react/index.js 文件包含了模块导入语句 require

// node_module/react/index.js
'use strict';if (process.env.NODE_ENV === 'production') {module.exports = require('./cjs/react.production.min.js');
} else {module.exports = require('./cjs/react.development.js');
}

此时,真正有效的代码被包含在 react.development.js(或 react.production.min.js)中,但 Webpack 只会打包这段 index.js 内容,也就造成了产物中实际上并没有真正包含 React。针对这个问题,我们可以先找到适用的代码文件,然后用 resolve.alias 配置项重定向到该文件:

// webpack.config.js
module.exports = {// ...module: {noParse: /react|lodash/,},resolve: {alias: {react: path.join(__dirname,process.env.NODE_ENV === "production"? "./node_modules/react/cjs/react.production.min.js": "./node_modules/react/cjs/react.development.js"),},},
};
  • 提示:使用 externals 也能将部分依赖放到构建体系之外,实现与 noParse 类似的效果,详情可查阅官网。

开发模式禁用产物优化

Webpack 提供了许多产物优化功能,例如:Tree-Shaking、SplitChunks、Minimizer 等,这些能力能够有效减少最终产物的尺寸,提升生产环境下的运行性能,但这些优化在开发环境中意义不大,反而会增加构建器的负担(都是性能大户)。

因此,开发模式下建议关闭这一类优化功能,具体措施:

  • 确保 mode='development'mode = 'none',关闭默认优化策略;
  • optimization.minimize 保持默认值或 false,关闭代码压缩;
  • optimization.concatenateModules 保持默认值或 false,关闭模块合并;
  • optimization.splitChunks 保持默认值或 false,关闭代码分包;
  • optimization.usedExports 保持默认值或 false,关闭 Tree-shaking 功能;
  • ……

最终,建议开发环境配置如:

module.exports = {// ...mode: "development",optimization: {removeAvailableModules: false,removeEmptyChunks: false,splitChunks: false,minimize: false,concatenateModules: false,usedExports: false,},
};

最小化 watch 监控范围

watch 模式下(通过 npx webpack --watch 命令启动),Webpack 会持续监听项目目录中所有代码文件,发生变化时执行 rebuild 命令。

不过,通常情况下前端项目中部分资源并不会频繁更新,例如 node_modules ,此时可以设置 watchOptions.ignored 属性忽略这些文件,例如:

// webpack.config.js
module.exports = {//...watchOptions: {ignored: /node_modules/},
};

跳过 TS 类型检查

JavaScript 本身是一门弱类型语言,这在多人协作项目中经常会引起一些不必要的类型错误,影响开发效率。随前端能力与职能范围的不断扩展,前端项目的复杂性与协作难度也在不断上升,TypeScript 所提供的静态类型检查能力也就被越来越多人所采纳。

不过,类型检查涉及 AST 解析、遍历以及其它非常消耗 CPU 的操作,会给工程化流程带来比较大的性能负担,因此我们可以选择关闭 ts-loader 的类型检查功能:

module.exports = {// ...module: {rules: [{test: /\.ts$/,use: [{loader: 'ts-loader',options: {// 设置为“仅编译”,关闭类型检查transpileOnly: true}}],}],}
};

有同学可能会问:“没有类型检查,那还用 TypeScript 干嘛?”,很简单,我们可以:

  1. 可以借助编辑器的 TypeScript 插件实现代码检查;

  2. 使用 fork-ts-checker-webpack-plugin 插件将类型检查能力剥离到 子进程 执行,例如:

    const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');module.exports = {// ...module: {rules: [{test: /\.ts$/,use: [{loader: 'ts-loader',options: {transpileOnly: true}}],}, ],},plugins:[// fork 出子进程,专门用于执行类型检查new ForkTsCheckerWebpackPlugin()]
    };
    

这样,既可以获得 Typescript 静态类型检查能力,又能提升整体编译速度。

优化 ESLint 性能

ESLint 能帮助我们极低成本发现代码风格问题,维护代码质量,但若使用不当 —— 例如在开发模式下使用 eslint-loader 实现实时代码检查,会带来比较高昂且不必要的性能成本,我们可以选择其它更聪明的方式接入 ESLint。

例如,使用新版本组件 eslint-webpack-plugin 替代旧版 eslint-loader,两者差异在于,eslint-webpack-plugin 在模块构建完毕(compilation.hooks.succeedModule 钩子)后执行检查,不会阻断文件加载流程,性能更优,用法:

  1. 安装依赖:

    yarn add -D eslint-webpack-plugin
    
  2. 添加插件:

    const ESLintPlugin = require('eslint-webpack-plugin');
    module.exports = {// ...plugins: [new ESLintPlugin(options)],// ...
    };
    

或者,可以选择在特定条件、场景下执行 ESLint,减少对构建流程的影响,如:

  • 使用编辑器插件完成 ESLint 检查、错误提示、自动 Fix,如 VS Code 的 dbaeumer.vscode-eslint 插件;
  • 使用 husky,仅在代码提交前执行 ESLint 代码检查;
  • 仅在 production 构建中使用 ESLint,能够有效提高开发阶段的构建效率。

慎用 source-map

source-map 是一种将经过编译、压缩、混淆的代码映射回源码的技术,它能够帮助开发者迅速定位到更有意义、更结构化的源码中,方便调试。不过,source-map 操作本身也有很大构建性能开销,建议读者根据实际场景慎重选择最合适的 source-map 方案。

针对 source-map 功能,Webpack 提供了 devtool 选项,可以配置 evalsource-mapcheap-source-map 等值,不考虑其它因素的情况下,最佳实践:

  • 开发环境使用 eval ,确保最佳编译速度;

  • 生产环境使用 source-map,获取最高质量。

  • 参考:webpack.js.org/configurati…

设置 resolve 缩小搜索范围

Webpack 默认提供了一套同时兼容 CMD、AMD、ESM 等模块化方案的资源搜索规则 —— enhanced-resolve,它能将各种模块导入语句准确定位到模块对应的物理资源路径。例如:

  • import 'lodash' 这一类引入 NPM 包的语句会被 enhanced-resolve 定位到对应包体文件路径 node_modules/lodash/index.js
  • import './a' 这类不带文件后缀名的语句,则可能被定位到 ./a.js 文件;
  • import '@/a' 这类化名路径的引用,则可能被定位到 $PROJECT_ROOT/src/a.js 文件

需要注意,这类增强资源搜索体验的特性背后涉及许多 IO 操作,本身可能引起较大的性能消耗,开发者可根据实际情况调整 resolve 配置,缩小资源搜索范围,包括:

1. resolve.extensions 配置:

例如,当模块导入语句未携带文件后缀时,如 import './a' ,Webpack 会遍历 resolve.extensions 项定义的后缀名列表,尝试在 './a' 路径追加后缀名,搜索对应物理文件

在 Webpack5 中,resolve.extensions 默认值为 ['.js', '.json', '.wasm'] ,这意味着 Webpack 在针对不带后缀名的引入语句时,可能需要执行三次判断逻辑才能完成文件搜索,针对这种情况,可行的优化措施包括:

  • 修改 resolve.extensions 配置项,减少匹配次数;

  • 代码中尽量补齐文件后缀名;

  • 设置 resolve.enforceExtension = true ,强制要求开发者提供明确的模块后缀名,不过这种做法侵入性太强,不太推荐。

2. resolve.modules 配置:

类似于 Node 模块搜索逻辑,当 Webpack 遇到 import 'lodash' 这样的 npm 包导入语句时,会先尝试在当前项目 node_modules 目录搜索资源,如果找不到,则按目录层级尝试逐级向上查找 node_modules 目录,如果依然找不到,则最终尝试在全局 node_modules 中搜索。

在一个依赖管理良好的系统中,我们通常会尽量将 NPM 包安装在有限层级内,因此 Webpack 这一逐层查找的逻辑大多数情况下实用性并不高,开发者可以通过修改 resolve.modules 配置项,主动关闭逐层搜索功能,例如:

// webpack.config.js
const path = require('path');module.exports = {//...resolve: {modules: [path.resolve(__dirname, 'node_modules')],},
};

3. resolve.mainFiles 配置:

resolve.extensions 类似,resolve.mainFiles 配置项用于定义文件夹默认文件名,例如对于 import './dir' 请求,假设 resolve.mainFiles = ['index', 'home'] ,Webpack 会按依次测试 ./dir/index./dir/home 文件是否存在

因此,实际项目中应控制 resolve.mainFiles 数组数量,减少匹配次数

总结

  • Webpack 在应对大型项目场景时通常会面临比较大的性能挑战,也因此非常值得我们投入精力去学习如何分析、优化构建性能,除了缓存、多进程构建这一类大杀器之外,还可以通过控制构建范围、能力等方式尽可能减少各个环节的耗时,包括文中介绍的:
    • 使用最新 Webpack、Node 版本
    • 约束 Loader 执行范围
    • 使用 noParse 跳过文件编译等
  • 如果下次再遇到性能问题,建议可以先试着分析哪些环节占用时长更多,然后有针对性的实施各项优化
  • 可以思考,除了上述各项优化外,还存在哪些有效措施?可以往 Webpack 的构建流程、组件等方向思考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/867423.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Lambda架构

1.Lambda架构对大数据处理系统的理解 Lambda架构由Storm的作者Nathan Marz提出,其设计目的在于提供一个能满足大数据系统关键特性的架构,包括高容错、低延迟、可扩展等。其整合离线计算与实时计算,融合不可变性、读写分离和复杂性隔离等原则&…

3.js - 裁剪平面(clipIntersection:交集、并集)

看图 代码 // ts-nocheck// 引入three.js import * as THREE from three// 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls// 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js// 导入tween import …

深度解析Ubuntu版本升级:LTS版本升级指南

深度解析Ubuntu版本升级:Ubuntu版本生命周期及LTS版本升级指南 Ubuntu是全球最受欢迎的Linux发行版之一,其版本升级与维护策略直接影响了无数用户的开发和生产环境。Canonical公司为Ubuntu制定了明确的生命周期和发布节奏,使得社区、企业和开…

Spring AOP源码篇三之 xml配置

简单代码示例, 了解Spring AOP基于xml的基本用法 xml配置&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:xsi"http://www.w3.org/2001/XMLSchema-insta…

django之url路径

方式一&#xff1a;path 语法&#xff1a;<<转换器类型:自定义>> 作用&#xff1a;若转换器类型匹配到对应类型的数据&#xff0c;则将数据按照关键字传参的方式传递给视图函数 类型&#xff1a; str: 匹配除了”/“之外的非空字符串。 /test/zvxint: 匹配0或任何…

golang线程池ants-实现架构

1、总体架构 ants协程池&#xff0c;在使用上有多种方式(使用方式参考这篇文章&#xff1a;golang线程池ants-四种使用方法)&#xff0c;但是在实现的核心就一个&#xff0c;如下架构图&#xff1a; 总的来说&#xff0c;就是三个数据结构&#xff1a; Pool、WorkerStack、goW…

【前端实现】在父组件中调用公共子组件:注意事项逻辑示例 + 将后端数组数据格式转换为前端对象数组形式 + 增加和删除行

【前端】在父组件中调用公共子组件的实现方法 写在最前面一、调用公共子组件子组件CommonRow.vue父组件ParentComponent.vue 二、实现功能1. 将后端数组数据格式转换为前端对象数组形式2. 增加和删除row 三、小结 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2…

全景图三维3D模型VR全景上传展示H5开发

全景图三维3D模型VR全景上传展示H5开发 3D互动体验平台的核心功能概览 兼容广泛格式&#xff1a;支持OBJ、FBX、GLTF等主流及前沿3D模型格式的无缝上传与展示&#xff0c;确保创意无界。 动态交互探索&#xff1a;用户可自由旋转、缩放、平移模型&#xff0c;深度挖掘每一处…

STMF4 硬件IIC(天空星开发板)

前言&#xff1a;笔记参考立创开发文档&#xff0c;连接放在最后 #IIC概念介绍 #IIC介绍 IIC通信协议&#xff0c;一种常见的串行通信协议&#xff0c;英文全程是 Inter-Integrated Circuit 使用这种通信方式的模块&#xff0c;通常有SCL&#xff08;Serial Clock Line&…

pytest使用报错(以及解决pytest所谓的“抑制print输出”)

1. 测试类的类名问题 #codingutf-8import pytestclass TestClass1:def setup(self) -> None:print(setup)def test_01(self) -> None:print(test_01111111111111111111111)def test_02(self) -> None:print(test_02)以上述代码为例&#xff0c;如果类名是Test开头&am…

Chair Footrest Protective Cover

Chair Footrest Protective Cover 万能通用型椅子脚垫保护套凳子耐磨硅胶加厚垫桌椅脚垫防滑静音套

Docker逃逸CVE-2019-5736、procfs云安全漏洞复现,全文5k字,超详细解析!

Docker容器挂载procfs 逃逸 procfs是展示系统进程状态的虚拟文件系统&#xff0c;包含敏感信息。直接将其挂载到不受控的容器内&#xff0c;特别是容器默认拥有root权限且未启用用户隔离时&#xff0c;将极大地增加安全风险。因此&#xff0c;需谨慎处理&#xff0c;确保容器环…

我使用HarmonyOs Next开发了b站的首页

1.实现效果展示&#xff1a; 2.图标准备 我使用的是iconfont图标&#xff0c;下面为项目中所使用到的图标 3. 代码 &#xff08;1&#xff09;Index.ets&#xff1a; import {InfoTop} from ../component/InfoTop import {InfoCenter} from ../component/InfoCenter import…

Mxnet转Onnx 踩坑记录

0. 前言 使用将MXNET模型转换为ONNX的过程中有很多算子不兼容&#xff0c;在此对那些不兼容的算子替换。在此之前需要安装mxnet分支v1.x版本作为mx2onnx的工具&#xff0c;git地址如下&#xff1a; mxnet/python/mxnet/onnx at v1.x apache/mxnet GitHub 同时还参考了如下…

【postgresql】 基础知识学习

PostgreSQL是一个高度可扩展的开源对象关系型数据库管理系统&#xff08;ORDBMS&#xff09;&#xff0c;它以其强大的功能、灵活性和可靠性而闻名。 官网地址&#xff1a;https://www.postgresql.org/ 中文社区&#xff1a;文档目录/Document Index: 世界上功能最强大的开源…

数据结构1:C++实现边长数组

数组作为线性表的一种&#xff0c;具有内存连续这一特点&#xff0c;可以通过下标访问元素&#xff0c;并且下标访问的时间复杂的是O(1)&#xff0c;在数组的末尾插入和删除元素的时间复杂度同样是O(1)&#xff0c;我们使用C实现一个简单的边长数组。 数据结构定义 class Arr…

web零碎知识2

不知道我的这个axios的包导进去没。 找一下关键词&#xff1a; http请求协议&#xff1a;就是进行交互式的格式 需要定义好 这个式一发一收短连接 而且没有记忆 这个分为三个部分 第一个式请求行&#xff0c;第二个就是请求头 第三个就是请求体 以get方式进行请求的失手请求…

Vatee万腾平台:智慧生活的无限可能

在科技日新月异的今天&#xff0c;我们的生活正被各种智能技术悄然改变。从智能家居到智慧城市&#xff0c;从个人健康管理到企业数字化转型&#xff0c;科技的力量正以前所未有的速度渗透到我们生活的每一个角落。而在这场智能革命的浪潮中&#xff0c;Vatee万腾平台以其卓越的…

Swagger php注解常用语法梳理

Swagger php注解常用语法梳理 快速编写你的 RESTFUL API 接口文档工具&#xff0c;通过注释定义接口和模型&#xff0c;可以和代码文件放置一起&#xff0c;也可以单独文件存放。 Swagger 优势 通过代码注解定义文档&#xff0c;更容易保持代码文档的一致性模型复用&#xff0…

C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例

C(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例 文章目录 C(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例1、概述2、实现效果3、主要代码4、源码地址 更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&#x1f449;GIS开发 &#x1f448; 1、概述 支持多线程加…