02-面试讲解
# Webpack构建优化面试讲解
# 技术点图谱
涉及的技术点如下:
- swc
- thread-loader
- webpack5 持久化缓存技术
- hash
- terser-webpack-plugin
# 难点描述
模拟问题:我看到你写了一个项目亮点,是优化 webpack 构建时间,这边关于 webpack 的打包流程,你有了解过么?
问题分析
这是一个偏向八股文的问题:
- 打包流程不要回答错
- 不断的往自己的项目上面去靠拢
参考答案
你好,老师,这边因为我要做 webpack 的构建优化,所以针对 webpack 的打包流程我是有所了解的。webpack 的打包流程大致可以分为以下几个步骤:
- 初始化:webpack 通过配置文件和 Shell 参数,初始化参数,确定入口文件、输出路径、加载器、插件等信息。接下来读取配置文件,并合并默认配置、CLI 参数等,生成最终的配置对象。
- 编译:从入口文件开始,递归解析模块依赖,找到所有需要打包的模块。之后使用 loader 对每个模块进行转换,转换成浏览器能够识别的 JS 代码。
- 构建模块依赖图:webpack 会为每个模块创建一个模块对象,并根据模块的依赖关系,生成一个模块依赖图(Dependency Graph)。
- 生成代码块(chunk):根据入口和依赖图,将所有模块分组,生成一个个包含多个模块的代码块(chunk),这些 chunk 会根据配置生成不同的输出文件。
- 输出:将生成的代码块输出到指定的文件夹,并根据配置生成对应的静态资源文件。
- 插件处理:在整个构建过程中,webpack 会在特定的生命周期钩子上执行插件,插件可以对打包的各个阶段进行干预和处理。
正是因为了解 webpack 整体的打包流程,所以我发现了很多可以优化的地方,然后进一步着力于构建的优化,大幅度缩减了构建所花费的时间。(钩子🪝)
模拟问题:我看到你写的构建时间从 8min 缩减至 10s 内,那么你能具体说一下你是如何做的么?还有你这里所写的所缩减的时间,是具体测量了的么?通过什么方式去测量的?
问题分析
- 简单介绍一下做这个事情的背景
- 通过什么工具去量化时间
- 你思考的方案以及最终落地的方案
- 落地方案的一个整体效果
参考答案
好的,那我把具体如何进行优化的,展开来讲一下。
我先说一下当时为什么需要做这个事儿吧。我们项目一开始就是基于 webpack 搭建的,随着后面项目越来越大,模块越来越多,构建的时间也越来越夸张。最开始还能够忍受,早上到公司先把项目跑起来,等它构建,然后开始小组会议,小组会议结束后回去差不多项目也跑起来了。但是后面随着模块越来越多,会都开完了回去项目都还在构建,于是我们小组讨论,这个问题必须得解决了,因为整个团队里面,就我对工程化有一些研究,所以指派我来完成这项任务。(简单阐述做这个事情的背景)
我当时首先想到了 Vite,因为 Vite 可以直接跳过打包的步骤,但是因为我们这个项目基于 webpack,体量很大,里面用到了很多 webpack 生态的插件和 loader,冒然迁移到 Vite 可能会带来一些未知的风险,例如在 Vite 生态中找不到对标的插件,那么我们就需要在 Vite 中通过自定义插件来实现那个插件的功能,这个工作量是不可预估的。(强调你是如何思考的)
所以,既然无法切换构建工具,那意味着无法跳过打包这个过程,因此我需要知道是哪些地方消耗了那么多时间。在 webpack 中有一个插件 speed-measure-webpack-plugin,我就是通过这个插件去查看的构建时间,它会出一份报告,包含总体构建时间、各阶段的耗时、插件耗时、loader 耗时,这样我就能非常清楚究竟是哪些地方耗时。
通过插件的分析结果来看,我发现 Babel 在编译 JS 时特别耗时,还有就是一些 loader,比如处理 CSS 的 css-loader,在解析和处理过程中也挺耗时的。因此我考虑的主要优化方案有:
- 用 swc 替换 babel 进行编译工作
- thread-loader 解决 loader 解析耗时问题
(你思考出来的方案是什么)
另外还配合了一些额外的优化手段。(钩子🪝)最终落地的方案效果非常好,再次用 speed-measure-webpack-plugin 插件进行构建时间分析,基本上构建时间在 10 多秒左右。(落地方案的一个整体效果)
就看老师您是否需要我把这些优化手段展开来说一下不?(钩子🪝)
# 技术点叙述
# 1. swc
模拟问题:你说你用到了 swc,那么就先讲一讲这个吧,什么是 swc,它的优势有什么?你为什么用它来替换 babel ?
问题分析
- 简单介绍什么是 swc
- 结合你的项目说一下优化前后的区别
- 放下一个钩子
参考答案
swc 是一个用 rust 写的 JS/TS 编译器,因为基于 rust,所以编译速度非常快,而且 swc 能够兼容大多数 babel 插件和配置,因此迁移起来没有太高的成本。(做一个简单的介绍)
做开始做优化之前,我们项目的构建时间差不多要花费 8~10分钟左右,替换为 swc 编译后,构建时间减少到了约 3 分钟左右。(优化前后的区别)
所以整个优化方案中 swc 是最重要,占大头的。而 loader 解析耗时的问题,我是通过 thread-loader 来解决的,thread-loader 不知道面试老师您之前了解过么?(钩子🪝)
# 2. thread-loader
模拟问题:你这边说一下呢?它是如何解决 loader 解析耗时问题的?
问题分析
- 介绍一下 thread-loader
- 结合你的项目,用了后的效果
- 抛出下一个钩子
参考答案
thread-loader 可以通过多线程并行处理 loader 操作,这样就减少了主线程的负载。(简单介绍)
当我们用了 thread-loader 以后,处理图片、CSS 相关 loader 的耗时问题也就解决了,构建时间进一步缩减到了 2 分钟。(使用后效果)
所以 swc 和 thread-loader 是整个优化方案中最核心的手段,剩下一些其他的优化手段,倒不是说没用,但主要是起到一个锦上添花的作用,一步一步将 2 分钟的构建时间继续优化到 10s 以内。就看老师您这边需要我介绍额外的那些优化手段不?
# 3. 持久化缓存和 hash
模拟问题:可以,你就挨着挨着讲吧?讲一下你剩下的那些优化手段,是如何将目前的 2 分钟优化到最后 10s 以内的?
问题分析
介绍剩余的优化手段即可。这里先说两个,看一下面试官的反馈。
参考答案
当时我们的项目,使用的 webpack 版本是 webpack5,所以我想到了可以利用 webpack5 的持久化缓存技术,来进一步压缩构建的时间。webpack5 新提供的持久化缓存技术,能够把模块的编译结果、解析结果以及插件的执行结果缓存到内存或者文件里面,这样后续进行构建的时候就可以重用这些缓存数据,减少不必要的重复计算和编译。
当然,第一次启动项目的时候,时间上面没有太大变化,因为缓存还没有生成,但是之后缓存非常有用,特别开发环境下需要频繁构建,如果每次都是构建所有的文件会非常耗时,而缓存的存在能进一步缩短构建时间,从 2 分钟缩短到了 1 分钟。
加上了缓存后,当时感觉这套优化方案已经是极限了,并且整个小组的成员也非常满意,毕竟之前构建需要等待 8 分钟,现在只需要 1 分钟左右,大家感觉这已经很好了。(该话术主要是增加一个真实性)
但是我这个人,做事情喜欢尽自己的能力,尽量做到最好,所以我就在想还有没有能进一步优化的手段,那段时间在网上搜索相关的资料,看了很多技术文章,后来发现在开发环境下通过去掉打包 hash 能进一步压缩开发环境下的构建时间。(突出个人做事儿的风格或者说态度)
因为在开发环境中,不太需要 hash,hash 实际上是生产环境下利用浏览器缓存机制优化用户体验的一种手段。开发环境下会频繁的进行代码修改和构建,而生成 hash 会涉及到额外的计算和文件处理,增加不必要的构建时间。
去除 hash 后,开发环境下的构建时间进一步优化到 40 秒。
说实话,一开始我对 hash 并不是太在意的,但是去掉 hash 后,真的还进一步压缩了开发环境下的构建时间,我发现很多东西无法一概而论,不同类型、规模的项目,在 A 项目中毫无问题的一些东西,在 B 项目中可能就会存在一些问题或者隐患。这也是我通过这个项目所积累的一个宝贵经验。(该话术介绍你在该项目中沉淀下来的一些东西,凸显一个真实性)
最后一个优化手段就是升级一些老旧的插件,因为一些插件新版本相较于旧版本,会修复一些 bug,在性能、算法上面可能也会有一些提升。
# 4. 浏览器缓存机制
模拟问题:我看你刚才提到了浏览器缓存,你来具体说一下呢?浏览器缓存有哪些类型?不同的类型是在什么时候命中的?
问题分析
- 描述什么是浏览器缓存
- 什么是强缓存,什么是弱缓存,各自的命中机制
- 再次抛出插件的钩子
参考答案
浏览器缓存是浏览器内部的一种机制,用于缓存 HTML、CSS、JS 以及图片等资源,这样可以减少服务器负担,加快页面加载速度。浏览器缓存主要分为两种类型:强缓存和弱缓存。
- 强缓存:是指浏览器在一定时间内直接从缓存中读取资源,而不发起请求到服务器。即使用户刷新页面,浏览器也不会向服务器请求资源。强缓存主要通过 HTTP 响应头中的 Expires 和 Cache-Control 字段来控制。
- 弱缓存:弱缓存则是在强缓存没有命中时,会去检查弱缓存。弱缓存也被称之为协商缓存,浏览器在请求资源时,会先发送一个请求到服务器,询问资源是否有更新。如果资源没有更新,服务器会返回 304 状态码,告诉浏览器继续使用缓存,这就是命中了弱缓存。如果资源有更新,服务器则返回新的资源内容。弱缓存主要通过 HTTP 响应头中的 Last-Modified 和 ETag 字段来控制。
一般对于不经常变化的静态资源,如网站的图片、样式表、JS 库等,通常会使用强缓存,并设置较长的缓存时间。对于经常变化的动态资源,如 API 响应、用户数据等通常使用弱缓存。(介绍什么是浏览器缓存)
所以生产环境下为了充分利用浏览器缓存机制,一般会生成 hash 值,这样可以确保文件名唯一,当文件内容发生变化时,文件名也会变化,从而强制浏览器加载新的资源。但是开发环境下不需要,开发环境下代码频繁更改,生成 hash 反而浪费时间,特别我们的项目规模又比较大,去掉 hash 后构建时间居然有 20s 左右的优化,从 1min 降到了 40s 左右。
最后升级了一下插件,达到了最终的 10s 以内的效果。(钩子🪝)
# 5. terser-webpack-plugin
模拟问题:那你说一下你升级了什么插件?
问题分析
- 简单回答升级了什么插件
- 对整套落地方案进行一个总结
参考答案
当时升级了一个 terser-webpack-plugin 的插件,这个插件是做压缩的。因为打包的时候有一个重要的任务就是要压缩,而压缩所花费的时间也挺长的,我就琢磨着能不能减少一下压缩所花费的时间。
一开始想着了解一下这个插件是如何压缩的,压缩算法是什么。结果刚好看到了这个插件的 CHANGELOG 文档,发现从 5.2.0 版本开始引入了 swc 压缩器,我就估摸着性能上面能有大幅的提升,于是我就对这个插件进行了版本升级,不出所料,压缩的时间再一次得到了优化,到了目前的 10s 左右。
现在回想起这一整套构建优化的落地方案,从最初的 8min 降到 10s 以内,我还是挺有心得体会的。一开始只想着如何加快构建速度,但性能优化往往是各种方面的,降低构建时间是一个方面,合理的利用缓存也是一种有效的手段,使用多线程来规避耗时任务阻塞主线程,也是一种方案。(阐述自己的心得体会,也是对整个方案做一个总结)
总之这就是我针对 webpack 构建优化沉淀下来的一套落地方案,不知道老师您那边还有什么更好的提议不?
(结束🎉)
-EOF-