前端性能优化是确保 Web 应用在各种设备和网络环境下都能保持快速、流畅体验的关键。优化的策略可以从多个方面来着手,总体上可以从网络层面和渲染层面下手,最终目的可以从体积层面和时间层面两个方面下手。
一、网络层面
网络层面可以从一下四个角度出发:
- 构建策略:基于构建工具(Webpack/Rollup/Parcel/Esbuild/Vite/Gulp)
- 图像策略:基于图像类型(JPG/PNG/SVG/WebP/Base64)
- 分发策略:基于内容分发网络(CDN)
- 缓存策略:基于浏览器缓存(强缓存/协商缓存)
其中的构建策略和图像策略都是在开发阶段进行的,而分发策略和缓存策略是在生产阶段进行的。
构建策略
代码分割
将大型文件拆分成多个小文件,按需加载,提升初始加载速度。
懒加载和动态导入
使用 import() 动态导入模块,延迟加载不需要在首屏渲染时加载的代码。
const module = import('./module.js');
Tree Shaking
Tree shaking 是移除未使用代码的一种技术。Webpack 默认支持它,可以通过配置 mode: 'production' 自动开启 tree shaking。
module.exports = {
mode: 'production',
};
压缩和优化
压缩代码、优化资源,减少文件体积,提升加载速度。
图像策略:
主要可以从图像类型和图像压缩两个方向入手。
1. 图像类型
了解不同图像类型的特点以及选择合适的图像类型尤为重要。如下图:
2. 图像压缩
压缩图片可以有效减小文件大小,提升加载速度。
- 无损压缩:例如使用工具如 ImageOptim、OptiPNG 进行无损压缩,减少文件体积而不损失画质。
- 有损压缩:对于大多数网络图片,可以使用有损压缩(如 JPEG 压缩),在保证视觉效果的前提下减小图片体积。
除此之外还有一个是精灵图。
3. 精灵图
精灵图(Sprite Sheet)是一种图像文件,它将多个小的图像整合成一个较大的单一图像,以提高性能和资源的利用效率。但是由于精灵图是通过定位的形式截取到每一个图标,所以十分依赖于图标的位置,如果要添加一个新图标或者改动其中的一个图标,那么可能其它图标的位置全部都会发生变化,代码全都要改,所以具有一定的局限性。
分发策略:
该策略主要围绕内容分发网络做相关处理,同时也是成本较高的性能优化策略,需足够资金支持。
CDN(内容分发网络,Content Delivery Network)是一种通过分布在全球各地的服务器来加速网页内容、视频、图片和其他静态资源的加载速度的技术。其核心目的是通过就近的服务器提供内容,从而减少用户和服务器之间的距离,提升访问速度、降低延迟,甚至减轻源服务器的压力。
CDN的工作原理:
- 分布式服务器:CDN 网络由多个位于不同地理位置的服务器组成,这些服务器缓存着网站的静态内容(如图片、视频、样式表、JavaScript 文件等)。
- 请求路由:当用户访问网站时,CDN 会根据用户的位置,自动将请求路由到距离用户最近的服务器,以减少网络延迟。( 就近原则 )
- 缓存机制:静态内容会被缓存到 CDN 服务器中,当内容发生更新时,CDN 会自动同步到各个节点。
基于CDN的就近原则所带来的优点,可将网站所有静态资源全部部署到CDN服务器里。通常来说就是那些无需服务器产生计算就能得到的资源,例如不常变化的样式文件、脚本文件和多媒体文件(字体/图像/音频/视频)等。
缓存策略:
在 Web 开发中,缓存是优化性能和减轻服务器负担的重要技术手段。HTTP缓存主要有强缓存和协商缓存,是根据HTTP头部来控制的两类缓存机制。
强缓存
强缓存是指在浏览器会根据HTTP头部,来判断是否在缓存有效期内,如果在缓存有效期内,浏览器则直接在缓存中获取数据资源,而不需要向服务器发送请求的一种方式。
关键 HTTP 头部:
- Expires:表示缓存的过期时间。它的值是一个具体的日期和时间,浏览器在这个时间之前会直接从缓存读取资源。
例子:Expires: Thu, 01 Dec 2025 16:00:00 GMT
-
Cache-Control:提供了更细粒度的缓存控制,比 Expires 更灵活。Cache-Control 中常用的指令有:
-
max-age=< seconds >:定义资源在缓存中存储的最大时间(以秒为单位)。例如:Cache-Control: max-age=3600 表示资源可以在缓存中存储 1 小时。
-
public:表示资源可以被任何缓存服务器缓存(如 CDN)。
-
private:表示资源只能在客户端缓存,不能被共享缓存服务器缓存。
-
no-store:不缓存任何内容。
-
no-cache:每次请求时都需要进行缓存验证,但缓存仍然存在。
-
协商缓存
每次使用缓存前,浏览器都要先发一次请求给服务器,通过与服务器进行协商,服务器根据这些信息判断缓存是否需要更新。如果缓存有效,服务器会返回 304 状态码,告诉浏览器继续使用本地缓存;如果缓存无效,服务器会返回新的资源。
关键 HTTP 头部:
-
Last-Modified:表示资源的最后修改时间,服务器会返回该时间戳给客户端。客户端在下一次请求时,会带上 If-Modified-Since 请求头,服务器根据该时间戳判断是否返回新的资源。
请求示例:If-Modified-Since: Mon, 20 Jan 2025 15:00:00 GMT 响应示例:Last-Modified: Mon, 20 Jan 2025 15:00:00 GMT
-
ETag:是资源的唯一标识符,通常是基于文件内容生成的哈希值。客户端在请求时,会带上 If-None-Match 请求头,服务器根据该标识符判断资源是否变更。
请求示例:If-None-Match: "etag_value" 响应示例:ETag: "etag_value"
二、渲染层面
减少页面的回流和重绘
什么是回流和重绘?
-
回流:是指浏览器重新计算元素的位置、大小、布局等信息,并重新渲染整个页面或部分页面的过程。
-
重绘:是指浏览器更新页面中元素的外观(颜色、背景等)而不改变其布局或结构的过程。
怎么优化?
1. 批量修改 DOM:
避免频繁修改 DOM:每次对 DOM 的修改都会触发回流和重绘,因此应尽量将多次 DOM 操作合并为一次。
2. 使用 class 而不是逐个修改样式:
如果要修改多个样式属性,最好将样式定义在 CSS 类中,修改元素的类名,而不是直接修改每个样式属性。这减少了浏览器的计算量。 例如,不要这样写:
element.style.width = '200px';
element.style.height = '100px';
而应该修改类:
element.classList.add('new-class');
3. 优化 CSS 动画和过渡:
避免使用会引发回流的属性:在 CSS 动画和过渡中,尽量使用不会触发回流的属性(如 transform 和 opacity),因为它们通常只会触发重绘,而不会导致回流。 而像 width、height、top 等属性会导致回流,性能较差。
4. 减少会引起回流动作的行为发生
可以将复杂的动画元素定位设置为 fixed 或 absoult
根据不同的网站类型选取不同的渲染方式
服务端渲染 (SSR) 和 客户端渲染 (CSR) 是两种常见的网页渲染方式,它们主要区别在于网页内容是在哪里生成的(服务器端或客户端)以及渲染的时机。
服务端渲染 (SSR, Server-Side Rendering)
服务端渲染指的是在服务器端生成 HTML 页面内容,然后将已渲染好的页面发送到浏览器。这意味着,所有的渲染工作(包括数据加载、HTML 生成、页面布局等)都发生在服务器上,浏览器接收到完整的 HTML 后直接展示给用户。
优点:
- SEO 更友好:因为搜索引擎可以轻松抓取服务器返回的完整 HTML 内容,而不需要额外的 JavaScript 执行。
- 更快的首次渲染时间:因为 HTML 已经被预先生成,浏览器只需要显示内容即可,相对减少了用户等待时间。
- 适用于内容不常变化的站点:例如新闻网站、博客等,内容较为静态且不依赖频繁的用户交互。
缺点:
- 服务器压力大:每次用户请求时都需要重新渲染页面,尤其在流量大的情况下,服务器负担较重。
- 较慢的交互体验:虽然初始页面加载快,但页面加载后需要通过 JavaScript 启动交互,可能会出现“闪烁”现象。
- 不适合复杂交互:动态交互和状态更新会变得比较复杂,需要额外的处理。
工作流程:
- 用户请求网页。
- 服务器接收到请求,渲染 HTML 页面。
- 服务器将渲染后的 HTML 返回给浏览器。
- 浏览器直接展示完整的页面,用户可见内容。
客户端渲染 (CSR, Client-Side Rendering)
客户端渲染则是将页面的渲染过程交给浏览器(客户端)来完成。在客户端渲染中,浏览器通过加载 JavaScript 代码,并向服务器请求数据,然后在客户端生成 HTML 内容,展示给用户。
优点:
- 交互性强:页面加载后,所有的交互操作可以快速响应,因为 JavaScript 可以在客户端控制页面的所有更新,不需要频繁与服务器通信。
- 服务器负担较轻:服务器只需要返回基础的 HTML 文件和数据,渲染工作交给浏览器进行。
- 适合单页应用(SPA):适合需要频繁切换页面、动态更新数据的应用,用户体验更流畅。
缺点:
- SEO 不友好:由于页面内容是通过 JavaScript 渲染的,搜索引擎可能无法完全抓取和索引页面内容,导致 SEO 表现差。
- 首次加载慢:页面的 HTML 是空的,浏览器需要加载 JavaScript 文件、执行脚本、获取数据并渲染页面,整个过程可能比较慢。
- 性能问题:在低性能设备或网络环境下,JavaScript 渲染可能会影响用户体验。
工作流程:
- 用户请求网页。
- 服务器返回一个包含 JavaScript 文件和空 HTML 的页面。
- 浏览器下载并执行 JavaScript 文件。
- JavaScript 向服务器请求数据(通过 API 等)。
- 浏览器通过 JavaScript 渲染页面内容,展示给用户。
总结:
script标签的加载方式
优化 < script > 标签的加载方式对页面渲染性能有很大的影响。JavaScript 通常是影响页面加载和渲染速度的瓶颈之一,特别是在大量依赖 JavaScript 渲染内容的现代网页中。以下是几种常见的优化
1. defer 属性
defer 属性会让浏览器在 HTML 文档解析完毕后再执行 JavaScript 文件,而不是阻塞 HTML 渲染。
优点:
- 不阻塞页面渲染:页面内容会继续渲染,不会等 JavaScript 文件下载和执行。
- 按顺序执行:即使有多个 < script > 标签,带 defer 的脚本会按照它们在 HTML 中的顺序依次执行。
<script src="script.js" defer></script>
适用场景:
适合需要在页面加载完成后执行的脚本(如一些初始化脚本、交互脚本等)。
2. async 属性
async 属性会让浏览器下载 JavaScript 文件时不阻塞页面渲染,并且脚本会在下载完成后立即执行。这意味着脚本的执行顺序不一定与在 HTML 中的顺序相同。
优点:
更快的加载速度:脚本和页面并行加载,页面渲染过程不会等待 JavaScript 加载。 适用于独立的脚本:如果脚本不依赖其他脚本或 DOM 内容,可以使用 async。
<script src="script.js" async></script>
适用场景:
适合第三方脚本(如广告、分析工具、外部库等),这些脚本不依赖页面的其他部分。
3. 将 < script > 标签放在页面底部
将 < script > 标签放在页面内容的底部(通常是在 </ body > 标签之前)可以确保 HTML 内容先渲染,然后再执行 JavaScript。这样,页面的视觉内容可以更快显示,避免了 JavaScript 文件的下载和执行阻塞页面的渲染。
<body>
<!-- 页面内容 -->
<script src="script.js"></script>
</body>
优点:
页面内容可以先展示给用户,提高首次渲染速度。 简单且有效的优化方法。
适用场景:
页面中的 JavaScript 不需要在初始渲染时立即执行。
4. 内联 JavaScript 代码
将 JavaScript 代码直接嵌入到页面中,而不是通过外部文件来加载,避免了额外的 HTTP 请求,从而提高页面加载速度。
<script>
// 内联的 JavaScript 代码
console.log("Page Loaded");
</script>
优点:
消除外部文件的请求,减少网络延迟。 对于小规模的脚本(如初始化代码、配置等),内联方式可以有效减少加载时间。
缺点:
如果 JavaScript 代码过多,会增加页面的大小,从而影响加载时间。 不便于缓存,尤其是当内容较多时。
适用场景:
小型脚本(如页面初始化、设置等)。
5. 分割 JavaScript 文件(代码分割)
通过代码分割(Code Splitting)将 JavaScript 分成多个小块,并仅在需要时加载。这种方式可以避免一次性加载整个 JavaScript 文件,优化页面加载性能。
使用现代 JavaScript 构建工具(如 Webpack、Rollup 或 Parcel)进行代码分割。 可以使用动态导入(import())来延迟加载某些模块。
// 示例:动态导入
import('script.js').then(module => {
// 使用模块
});
优点:
只加载页面渲染所需的最小代码,避免加载不必要的 JavaScript 文件。 适合大型应用,尤其是单页应用(SPA)。
适用场景:
适用于大型 JavaScript 应用,尤其是那些含有多个页面和模块的应用。
总结:
前端性能优化是一项综合性工作,需要考虑多个方面的因素,如网络请求、资源压缩、缓存策略、JavaScript 执行优化等。通过合理的优化策略,可以提高页面加载速度、降低延迟,从而提升用户体验。下一篇文章将进行讲解react中hook的性能优化。
参考文章: