面试题之前端性能调优

82 阅读10分钟

基本步骤

1. 性能评估和基准测试

  • 收集指标:使用工具(比如Lighthouse、WebPageTest、Chrome DevTools)进行性能评估,确定当前性能的基线。关注的指标包括首次内容绘制(FCP)、最大内容绘制(LCP)、交互时间(TTI)、阻塞时间(TBT)和累积布局偏移(CLS)等。
  • 用户体验指标:了解实际用户如何体验应用,可能通过Real User Monitoring(RUM)工具来收集数据。

2. 识别瓶颈

  • 性能分析:通过上述工具的分析报告,我会识别出哪些资源加载缓慢、哪些操作引发重绘和回流、以及脚本执行时间长等问题。
  • 代码审查:结合性能分析的结果,我会进行代码审查,寻找可能的性能瓶颈,如不必要的数据计算、频繁的状态更新、大型组件的重渲染等。

3. 优化策略制定

  • 优先级排序:根据影响程度和优化难度,为找到的问题排序,先解决那些“低挂果实”(即容易解决且影响大的问题)。
  • 制定计划:确定具体的优化措施,如代码分割、懒加载、图片和资源优化、缓存策略、减少请求次数、服务端渲染(SSR)或静态站点生成(SSG)等。

4. 实施优化

  • 逐项优化:根据制定的计划,逐个实施优化措施。这可能涉及重构代码、更换库或框架、修改资源加载策略等。
  • 性能预算:为项目设置性能预算,确保未来的开发不会影响到已有的性能优化成果。

具体操作

首次内容绘制(FCP)

  • 优化关键渲染路径:减少关键资源的数量和大小,使用异步加载避免阻塞渲染。
  • 服务器响应时间:通过使用CDN、缓存策略和优化服务器配置来提高服务器响应速度。
  • 优化CSS:确保CSS文件尽可能小,并通过媒体查询(media queries)推迟非关键CSS的加载。

最大内容绘制(LCP)

  • 图像优化:为不同屏幕尺寸提供适当大小的图像,使用现代格式如WebP,并实施懒加载策略。
  • 前端资源优化:通过代码分割和路由级别的懒加载来减少初始加载大小。
  • 服务端渲染(SSR)或生成静态站点(SSG) :通过服务端渲染或静态站点生成来加快首屏内容的呈现。

交互时间(TTI)

  • 减少JavaScript执行时间:分析并减少不必要的JavaScript执行,优化剩余的脚本。
  • 使用Web Workers:对于复杂计算,可以使用Web Workers来避免阻塞主线程。
  • 代码拆分和动态导入:只加载用户需要的代码,提高应用的响应能力。

总阻塞时间(TBT)

  • 优化长任务:查找并拆分长时间运行的JavaScript任务,使用时间切片技术。
  • 使用requestIdleCallback:利用浏览器的空闲时段执行低优先级任务,减少主线程的阻塞时间。
  • 减少JavaScript传输大小:通过压缩、摇树(tree shaking)和代码分割减少文件大小。

累积布局偏移(CLS)

  • 指定图像和视频尺寸:在HTML中预先指定媒体文件的尺寸,防止加载时的布局变动。
  • 避免插入动态内容:避免在页面上方插入动态内容,这可能会导致下方内容的移动。
  • 字体加载策略:使用font-display选项来控制字体的加载方式,避免字体加载导致的布局偏移。

资源优化

1. 优化图片和媒体文件

  • 压缩图片:使用工具如TinyPNG或ImageOptim来减少图片文件的大小,而不损失太多质量。
  • 使用现代图像格式:采用WebP、AVIF等现代图像格式,这些格式通常比传统的JPEG或PNG提供更好的压缩率。
  • 响应式图片:使用<picture>元素或srcset属性,为不同屏幕尺寸提供合适大小的图片。

2. 减少JavaScript和CSS的大小

  • 压缩和合并文件:使用工具如Webpack、Gulp或Rollup来压缩和合并JavaScript和CSS文件,减少请求次数和数据传输量。
  • 树摇(Tree-shaking) :移除未使用的代码。现代打包工具和ES6模块支持可以帮助自动完成这一点。
  • 代码分割:将代码分割成多个块,只为用户当前需要的内容加载相应的代码块。例如,在React中,可以使用React.lazySuspense来实现路由级的代码分割。

3. 使用异步加载

  • 异步加载脚本:使用<script async><script defer>加载JavaScript文件,以非阻塞方式执行脚本。async适用于那些不依赖于其他脚本且其他脚本也不依赖于它的脚本;defer适用于那些虽然需要执行,但执行顺序不重要的脚本。
  • 懒加载:对于非关键资源,如页面下方的图片或位于用户滚动位置以下的内容,可以使用懒加载技术。这意味着只有当用户滚动到这些内容的位置时,才开始加载它们。这可以用原生的loading="lazy"属性或JavaScript库来实现。

4. 优化字体加载

  • 选择优化的字体格式:使用WOFF2字体格式,它通常比传统的字体格式提供更好的压缩。
  • 字体子集化:只包含你的网站实际需要的字符,减少字体文件大小。
  • 字体加载策略:利用font-displayCSS属性来控制字体的加载行为,避免字体加载导致的文本不可见。

5. 使用Content Delivery Network (CDN)

  • 通过CDN分发内容:使用CDN可以减少资源到用户浏览器的延迟,特别是当用户与服务器地理位置较远时。

延伸

tree shaking 失效问题

在使用Tree Shaking功能时,有时你可能会发现某些本应被摇掉(即删除)的代码仍然存在于最终的bundle中。这可能是因为多种原因,包括配置错误、代码侧效应(side effects),或者是使用了某些不支持Tree Shaking的代码模式。以下是排查Tree Shaking无效问题的一些步骤和建议:

1. 检查ES模块语法

Tree Shaking依赖于ES模块的静态结构,确保你的代码和所有依赖都是使用ES模块的importexport语句,而非CommonJS的requiremodule.exports。转换为ES模块可以让Webpack等构建工具更容易地分析和摇掉未使用的代码。

2. 验证Webpack等构建工具的配置

确保你的构建工具配置正确地启用了Tree Shaking。例如,在Webpack中,你需要:

  • 使用mode: 'production',这会自动启用Tree Shaking。
  • 确保optimization.usedExports设置为true(在生产模式下默认为true)。

3. 检查代码中的副作用

代码副作用可能会阻止Tree Shaking。如果一个模块执行了副作用(如修改全局变量、立即执行的操作等),那么构建工具可能会认为这个模块是必需的,从而保留它。你可以在package.json中使用"sideEffects"属性来标记你的代码是否有副作用:

"sideEffects": false

或者,如果只有部分文件有副作用,你可以指定哪些文件有副作用:

"sideEffects": [
  "./src/someSideEffectfulFile.js"
]

4. 使用工具和插件分析bundle

利用Webpack的Bundle Analyzer插件或其他类似工具来可视化你的bundle内容。这可以帮助你识别哪些未被摇掉的代码仍然存在,以及它们被包含的原因。

5. 优化第三方库的使用

某些第三方库可能不支持Tree Shaking,特别是那些不使用ES模块的库。尝试寻找支持Tree Shaking的现代库,或者只从库中导入你需要的部分,以减少最终bundle的大小。

6. 手动标记未使用的导出

在一些情况下,即使你做了上述所有优化,仍然有一些代码不能被摇掉。你可能需要手动标记或删除未使用的代码,特别是在维护一个大型项目时。

7. 检查动态导入

动态导入(使用import()语法)可以创建新的chunk,这些chunk只会在需要时被加载。确保动态导入的使用不会意外地阻止Tree Shaking。

通过上述步骤,你可以有效地诊断和解决Tree Shaking不工作的问题,进一步优化你的应用性能。在面试中,能够展现出对这些优化技术的深入理解,会给面试官留下专业且细致的印象。

split chunks 模式

Webpack的SplitChunksPlugin是一个非常强大的插件,用于优化代码的分割和重用。通过配置optimization.splitChunks选项,你可以控制如何将代码分割成不同的chunks(代码块)。SplitChunksPlugin提供了几种分割代码的模式,通过这些模式,可以基于各种条件和策略将代码分割成更细粒度的chunks,以优化加载时间和浏览器缓存。以下是SplitChunksPlugin的一些关键配置选项,通过这些选项你可以实现不同的分割模式:

chunks选项

  • async:只从异步加载的模块中分割代码。这是默认行为,只会影响那些通过动态导入(如import())加载的模块。
  • initial:只从入口点开始的同步模块中分割代码。
  • all:从所有模块中分割代码,无论是同步还是异步加载的。这需要在缓存组(cacheGroups)层面做更细致的配置来确定如何分割代码。

cacheGroups选项

cacheGroupssplitChunks的一个核心概念,它允许你为不同类型的模块创建不同的缓存组,并为每个缓存组应用不同的分割策略。默认情况下,Webpack提供了vendorsdefault两个缓存组,但你可以根据需要定义更多。

  • vendors:通常用于将来自node_modules目录的模块分割到一个单独的chunk中,这有助于将第三方库代码与应用代码分开,优化缓存。
  • default:应用于应用代码的默认缓存组,可以调整其最小尺寸、最小chunks数量等选项,以决定何时创建新的chunk。

test, minSize, maxSize, minChunks等选项

这些选项允许你进一步细化每个缓存组的分割策略:

  • test:一个函数或正则表达式,用于匹配模块路径,决定哪些模块应该被包含在当前缓存组中。
  • minSize:生成chunk的最小尺寸。仅当模块的大小超过此值时,才会被分割出来。
  • maxSize:尝试将大于maxSize的chunk分割成更小的部分,以便更平滑地加载。
  • minChunks:一个模块被分割前在入口点中最少的引用次数。

如何选择模式

选择哪种模式或配置取决于你的应用需求和优化目标。例如,如果你想要更快地让首屏加载,可能会倾向于只分割异步加载的模块。如果你的应用依赖于许多大型第三方库,将这些库分割到单独的vendors chunk中可以提高缓存利用率。

在实践中,你可能需要根据实际的加载性能和用户体验来调整配置。使用Webpack的分析工具,如webpack-bundle-analyzer,可以帮助你可视化分割的结果,并根据分析数据调整配置。

在面试中讨论SplitChunksPlugin时,展示你如何根据项目需求选择和调整不同的分割模式,以及你是如何通过实验和性能分析来优化配置的,会非常有帮助。这显示了你不仅理解Webpack的高级功能,而且也关注实际的应用性能和用户体验。