谈谈前端性能优化

161 阅读13分钟

前端性能优化是确保 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. 图像压缩

压缩图片可以有效减小文件大小,提升加载速度。

  1. 无损压缩:例如使用工具如 ImageOptim、OptiPNG 进行无损压缩,减少文件体积而不损失画质。
  2. 有损压缩:对于大多数网络图片,可以使用有损压缩(如 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 头部:
  1. 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

  2. ETag:是资源的唯一标识符,通常是基于文件内容生成的哈希值。客户端在请求时,会带上 If-None-Match 请求头,服务器根据该标识符判断资源是否变更。

    请求示例:If-None-Match: "etag_value" 响应示例:ETag: "etag_value"

二、渲染层面

减少页面的回流和重绘

什么是回流和重绘?

  1. 回流:是指浏览器重新计算元素的位置、大小、布局等信息,并重新渲染整个页面或部分页面的过程。

  2. 重绘:是指浏览器更新页面中元素的外观(颜色、背景等)而不改变其布局或结构的过程。

怎么优化?

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 启动交互,可能会出现“闪烁”现象。
  • 不适合复杂交互:动态交互和状态更新会变得比较复杂,需要额外的处理。
工作流程:
  1. 用户请求网页。
  2. 服务器接收到请求,渲染 HTML 页面。
  3. 服务器将渲染后的 HTML 返回给浏览器。
  4. 浏览器直接展示完整的页面,用户可见内容。

客户端渲染 (CSR, Client-Side Rendering)

客户端渲染则是将页面的渲染过程交给浏览器(客户端)来完成。在客户端渲染中,浏览器通过加载 JavaScript 代码,并向服务器请求数据,然后在客户端生成 HTML 内容,展示给用户。

优点:
  • 交互性强:页面加载后,所有的交互操作可以快速响应,因为 JavaScript 可以在客户端控制页面的所有更新,不需要频繁与服务器通信。
  • 服务器负担较轻:服务器只需要返回基础的 HTML 文件和数据,渲染工作交给浏览器进行。
  • 适合单页应用(SPA):适合需要频繁切换页面、动态更新数据的应用,用户体验更流畅。
缺点:
  • SEO 不友好:由于页面内容是通过 JavaScript 渲染的,搜索引擎可能无法完全抓取和索引页面内容,导致 SEO 表现差。
  • 首次加载慢:页面的 HTML 是空的,浏览器需要加载 JavaScript 文件、执行脚本、获取数据并渲染页面,整个过程可能比较慢。
  • 性能问题:在低性能设备或网络环境下,JavaScript 渲染可能会影响用户体验。
工作流程:
  1. 用户请求网页。
  2. 服务器返回一个包含 JavaScript 文件和空 HTML 的页面。
  3. 浏览器下载并执行 JavaScript 文件。
  4. JavaScript 向服务器请求数据(通过 API 等)。
  5. 浏览器通过 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的性能优化

参考文章:

  1. segmentfault.com/a/119000004…
  2. xiaolincoding.com/network/2_h…