技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

前端架构与性能优化那些事儿

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/20-前端架构与性能优化那些事儿
本文目录14 个章节
  1. 零、写在前面
  2. 一、为什么要进行性能优化
  3. 二、历史上的“雅虎军规”
  4. 三、文件压缩、合并与 HTTP/2/HTTP/3
  5. 四、离线缓存:不要用 localStorage 存资源正文
  6. 五、缓存策略
  7. 六、浏览器解析、CSS、JavaScript 和渲染
  8. 七、渲染性能优化
  9. 八、从网络到渲染的实用优化清单
  10. 九、SPA、MPA、SSR 和混合架构
  11. 十、性能预算和持续回归
  12. 十一、原文中的错误和过时结论
  13. 总结
  14. 参考资料

前端架构与性能优化那些事儿

Category(分类): Browser, Performance, Architecture Status: 已更新

原文从“雅虎军规”、压缩与合并、HTTP/2、多种缓存、CSS/JS 阻塞、渲染流水线、GPU 加速、SPA/MPA 和离线缓存等角度讨论性能。本文尽量保留这些学习材料和题目,但删除固定连接数、错误的压缩结论、绝对化的 GPU 说法,并补充 HTTP/3、现代资源加载、Core Web Vitals、SSR/SSG、Service Worker 和代码分割。

零、写在前面

性能优化不是把某个数字调到最小,也不是背一份永远不变的军规。它是围绕用户任务,在网络、CPU、内存、渲染和业务约束之间做取舍:

测量真实问题 -> 找到瓶颈 -> 做最小改动 -> 验证收益 -> 持续回归

大型 B 端应用、移动端页面、内容站、后台管理系统和 Web 游戏的瓶颈不同。一个页面可能网络很快,却被 JavaScript 长任务阻塞;也可能主线程很空闲,但首屏图片太大或服务端 TTFB 很高。

一、为什么要进行性能优化

用户对加载速度和交互响应敏感,性能还可能影响转化、留存、可访问性和基础设施成本。原文引用的“3 秒”“每慢 1 秒损失多少”来自特定时间、样本和业务环境,不能当成所有网站都成立的普遍常数。

更稳妥的做法是建立自己的基线:

  • 关注 LCP、INP、CLS 等用户体验指标的 p75/p95;
  • 按设备、网络、地区、页面类型和版本分组;
  • 同时观察错误率、转化率、留存和服务器成本;
  • 先测量再优化,不要为了追求 Lighthouse 分数牺牲可用性和可维护性。

PV(Page View)是页面浏览量,UV(Unique Visitor)是去重后的访客数。它们是流量指标,不是性能指标,但可以和性能分布关联分析。

二、历史上的“雅虎军规”

原文提到的 Yahoo Performance Best Practices 在前端性能学习史上很有代表性:

Yahoo 性能优化规则示意

1. 内容和请求

历史建议:

  • 减少不必要的 HTTP 请求;
  • 减少 DNS 查找和重定向;
  • 让可缓存的 AJAX 响应可缓存;
  • 延迟加载非关键组件,预加载确定会使用的关键组件;
  • 减少不必要的 DOM 节点;
  • 避免 404 和无意义的空资源 URL;
  • 谨慎使用 iframe。

现代补充:

  • HTTP/2 和 HTTP/3 的多路复用降低了“合并所有文件”的必要性,但请求数量和响应字节仍有成本;
  • 重点是减少关键路径上的资源和字节,而不是机械追求“请求越少越好”;
  • 资源提示(preloadpreconnectprefetchfetchpriority)要依据真实关键路径使用,滥用会抢占真正关键资源;
  • 重定向会增加导航延迟,CDN、HTTPS、www 规范化和登录跳转应尽量减少链路;
  • 代码分割应按路由和交互边界拆分,不能为了减少初始包把用户点击后的所有内容都变成瀑布请求。

2. CSS

历史建议:

  • 样式表尽量放在文档头部;
  • 避免 CSS 表达式和旧式滤镜;
  • 谨慎使用 @import
  • 让关键 CSS 尽早可用。

现代补充:

  • <link rel="stylesheet"> 通常会阻塞首次渲染,因为浏览器需要 CSSOM 才能安全计算样式;
  • 不要把整个大型 CSS 框架作为所有页面的关键资源,可按路由拆分并移除未使用 CSS;
  • 关键 CSS 内联或 preload 需要用真实指标验证,内联过多会增加 HTML 和缓存成本;
  • 使用 content-visibilitycontain、虚拟列表等方式减少不可见大区域的初始工作,但要测试可访问性、滚动和查找行为;
  • CSS-in-JS 不是天然的性能优化,运行时注入样式也可能增加 JavaScript 和样式计算成本。

3. JavaScript

历史建议:

  • 移除重复脚本;
  • 减少不必要的 DOM 查询和强制布局;
  • 使用事件委托;
  • 把非关键脚本延后。

现代补充:

  • 优先使用 <script type="module">defer 或框架的代码分割;
  • async 适合互相独立的脚本,执行顺序不保证;defer 保持文档顺序,并在 HTML 解析完成后执行;
  • 降低初始 JavaScript 字节只是第一步,还要减少解析、编译、执行和水合成本;
  • 第三方脚本要有预算、延迟加载和失败隔离;
  • 长任务会阻塞输入和渲染,可以拆分任务、批量更新、使用 Worker 或延迟非关键计算。

4. 静态资源、Cookie 和服务器

历史建议:

  • 压缩 JS/CSS;
  • 优化图片和雪碧图;
  • 不要用 HTML 把大图缩小显示;
  • 减小 Cookie;
  • 使用 CDN;
  • 配置 Gzip、ETag、ExpiresCache-Control

现代补充:

  • 图片应使用合适尺寸和格式,提供 srcset/sizes,为非首屏图片使用 loading="lazy"
  • JPEG、PNG、WebP、AVIF 的选择取决于内容和浏览器支持,不能只看文件扩展名;
  • Brotli(br)和 Gzip 主要用于 HTML、CSS、JavaScript、JSON、SVG 等文本,通常不要再对已压缩的 JPEG/PNG 进行 HTTP Brotli/Gzip;
  • 内容指纹资源可以长缓存,HTML 入口需要及时验证;
  • CDN 不只是“多开几个连接”,还涉及距离、缓存命中、TLS、压缩、失效和回源策略;
  • Cookie 会自动发送到匹配的请求,静态资源可以使用独立无 Cookie 域,但要结合安全策略、CORS 和缓存分区设计;
  • ETag 和缓存策略要依据实际缓存链路配置,不要把旧的字段优先级表当成固定规则。

三、文件压缩、合并与 HTTP/2/HTTP/3

压缩和合并确实存在取舍:合并能减少请求,但可能让不同页面下载不需要的代码,也会降低缓存复用;拆分能实现按需加载,但拆得过细会形成请求瀑布。

现代项目应根据关键路径和构建产物做决定:

  • HTML、CSS、JS、JSON、SVG 等文本使用 Brotli 或 Gzip;
  • 生产构建移除调试代码、压缩语法、压缩 source map(source map 需单独保护);
  • 图片在源文件阶段压缩,并输出适合显示尺寸的变体;
  • 使用内容哈希文件名,让长期缓存和快速更新同时成立;
  • 对首屏必需资源使用有限的 preloadfetchpriority="high",不要给所有资源加高优先级。

BR 和 Gzip 的区别

Brotli(br)和 Gzip 都是 HTTP 内容编码,服务器根据请求的 Accept-Encoding 选择并返回 Content-Encoding。Brotli 在许多文本场景下压缩率较好,但压缩级别、CPU 成本和服务端配置要综合考虑。

Accept-Encoding: br, gzip

Content-Encoding: br
Vary: Accept-Encoding

“Brotli 对图片压缩效果非常好”是不准确的。JPEG、PNG 等格式已经有内部压缩,再套一层 Brotli/Gzip 通常收益很小,可能浪费 CPU;图片优化应在编码格式、质量、尺寸和响应式变体上进行。

HTTP/2

HTTP/2 可以在一条连接上复用多个带优先级的流,并提供头部压缩。它缓解了 HTTP/1.1 下队头阻塞和连接数管理的部分问题,但浏览器、代理和服务端仍然可能受连接、带宽和 HTTP/2 TCP 队头阻塞影响。

HTTP/2 多路复用示意

“同一域名所有请求永远只有一路连接”和“Apache 最大连接数因此直接提升 60 倍”都不是通用结论。浏览器可能因不同网络地址、代理、连接状态和协议协商建立多个连接。

HTTP/3

HTTP/3 使用 QUIC 和 UDP,提供基于流的传输,并减少 TCP 层队头阻塞的影响。是否使用 HTTP/3 由客户端、服务端和中间网络协商,不能在前端代码中假设它一定存在。

Keep-Alive 与 HTTP/2 多路复用也不是一回事:

  • HTTP/1.1 Keep-Alive 主要复用一条 TCP 连接,避免每个请求重新握手;
  • HTTP/2 在连接上复用多条流;
  • HTTP/3 使用 QUIC 连接和流。

四、离线缓存:不要用 localStorage 存资源正文

原文以 localStorage 保存资源映射,并提到 localForage、Service Worker 和 basket.js。这些材料体现了离线缓存的发展过程,但新项目不应把 localStorage 当作资源缓存层:它是同步 API,只存字符串,容量和淘汰受浏览器管理,保存大量脚本正文会阻塞主线程。

1. 推荐的现代分层

  • HTTP 缓存:由响应头控制,适合普通网络资源;
  • Service Worker + Cache API:适合离线 App Shell、静态资源和自定义网络策略;
  • IndexedDB:适合结构化业务数据、离线队列和较大数据;
  • localStorage:适合少量非敏感配置,不适合 Token、密集写入和大资源。

2. 版本化缓存示意

const CACHE_NAME = 'app-shell-v3'
const APP_SHELL = ['/', '/assets/app.8d31c2.js', '/assets/app.4fa91e.css']

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL))
  )
})

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys
        .filter(key => key !== CACHE_NAME)
        .map(key => caches.delete(key))
    ))
  )
})

self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return

  event.respondWith(
    caches.match(event.request).then(cached => {
      const network = fetch(event.request).then(response => {
        if (response.ok && response.type === 'basic') {
          const copy = response.clone()
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy))
        }
        return response
      })

      return cached || network
    })
  )
})

真实项目还要处理离线失败、缓存版本、更新提示、Opaque Response、空间配额和用户数据隐私。缓存静态资源时优先使用内容指纹 URL,避免旧缓存覆盖新版本。

五、缓存策略

原文给出了“cache-control > ecprise > etag > last-modified”的固定优先级。这个表存在拼写和概念问题:Expires 拼写错误,且新鲜度指令、验证器和请求条件不是同一层面的优先级链。

推荐按资源类型设计:

# 内容指纹 JS/CSS/字体
Cache-Control: public, max-age=31536000, immutable

# HTML 入口:允许存储,但复用前验证
Cache-Control: no-cache

# 个性化 API:只允许浏览器缓存并验证
Cache-Control: private, no-cache

# 不应被缓存保存的敏感响应
Cache-Control: no-store

ETagLast-Modified 配合 If-None-MatchIf-Modified-Since 可以返回 304 Not Modifiedno-cache 不等于禁止存储,no-store 也不会自动删除已经存在的旧响应。完整缓存解释见 前端也要懂 HTTP 缓存机制

六、浏览器解析、CSS、JavaScript 和渲染

1. JavaScript 会不会影响 HTML 解析和渲染?

原文示例:

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8">
    <title>脚本加载</title>
  </head>
  <body>
    <h1>标题</h1>
    <script>
      prompt('测试')
    </script>
  </body>
</html>

经典脚本在没有 async/defer 时会暂停 HTML 解析,下载和执行期间也会阻塞主线程;prompt() 还会暂停当前页面。它当然可能影响后续 HTML 解析和页面渲染。现代页面应避免同步长任务,并根据依赖关系使用:

<script src="/assets/analytics.js" async></script>
<script src="/assets/app.js" defer></script>
<script type="module" src="/assets/entry.js"></script>
  • async 脚本下载不阻塞解析,下载完成后立即执行,多个脚本顺序不保证;
  • defer 经典脚本在解析完成后执行,并保持文档顺序;
  • 模块脚本默认延迟执行并支持依赖图;
  • 模块或延迟脚本仍可能因 CSS、依赖和执行时间影响 DCL 和渲染。

2. CSS 会不会影响 HTML 解析和渲染?

CSS 通常是渲染阻塞资源:浏览器需要 CSSOM 才能安全构建渲染树。CSS 下载本身不一定让 HTML 解析完全停止,但会影响首次绘制;位于样式表之后的经典脚本可能等待样式表,因为脚本可能读取样式相关信息。

<link rel="stylesheet" href="/assets/app.css">
<script>
  // 在这个位置的经典脚本可能等待前面的样式表完成
  console.log(getComputedStyle(document.body).display)
</script>

CSS 后没有脚本时,样式表不一定阻止 HTML 解析;但“DOMContentLoaded 不直接等待样式表”也不代表 CSS 与 DCL 永远无关。延迟脚本会等待此前需要的样式表,样式计算和脚本执行可能间接延后 DCL。

不要用 setTimeout 证明 CSS 不阻塞解析:定时器只是把代码放进任务队列,不能消除资源依赖、主线程竞争或浏览器实现差异。

3. 关键渲染路径

现代渲染流水线示意

可以把首次渲染简化为:

HTML -> DOM
CSS  -> CSSOM
DOM + CSSOM -> Render Tree
Render Tree -> Style/Layout -> Paint -> Composite -> 屏幕

实际浏览器还会处理字体、图片、动画、滚动、合成层、可见区域和 GPU/CPU 任务。主线程、合成线程、栅格化线程和浏览器进程的划分属于实现细节,不应把某个版本的内部线程图当成所有浏览器都固定遵循的规则。

七、渲染性能优化

1. Layout、Paint 和 Composite

  • Style/Recalculate Style:计算元素最终样式;
  • Layout(布局):计算几何位置和尺寸;
  • Paint(绘制):把文本、边框、背景和阴影等绘制命令记录下来;
  • Rasterization(栅格化):把绘制命令变成像素,可能由多个线程或 GPU 协作完成;
  • Composite(合成):把不同合成内容按位置、透明度和变换组合到屏幕。

修改 widthheight、字体、内容和布局属性可能触发布局;修改颜色、阴影可能触发绘制;transformopacity 在满足条件时通常更容易只触发合成。但这些是常见优化路径,不是绝对规则,元素复杂度和浏览器实现都会影响结果。

2. 什么会创建合成层?

根元素、视频、canvas、3D transform、动画中的 opacity/transform、固定定位、滤镜和 will-change 等都可能影响层化,但“写了 position 就一定独立成层”“写了 transform 就一定绕过重绘”都不准确。浏览器会基于内容、动画、滚动和内存成本做启发式决策。

will-change 是给浏览器的提前提示,不是通用加速开关:

.carousel {
  will-change: transform;
}

.carousel.is-idle {
  will-change: auto;
}

长期给大量元素设置 will-change 会占用内存,甚至让性能变差。不要为了“GPU 加速”到处添加 translateZ(0);优先使用合适的 CSS 动画并用 Performance 面板验证。

3. 轮播图为什么常用 transform?

使用 transform 的轮播图示意

使用 left/top 移动元素可能触发布局或绘制;使用 transform: translateX() 在很多场景下更容易走合成路径:

.carousel-track {
  transform: translateX(var(--offset));
  transition: transform 240ms ease;
}

这不是“GPU 直接跳过 DOM、Layout、Paint 的万能方案”:

  • transform 只改变视觉变换,不改变文档流布局;
  • 大面积图层会增加栅格化和显存成本;
  • 文字、滤镜、透明度和重叠内容仍可能需要绘制;
  • 要限制同时存在的图层数量,并在真实设备上验证。

4. 避免强制同步布局

读取几何属性不是必然触发布局;当浏览器有未提交的样式/DOM 写入且读取需要最新几何结果时,可能发生 forced synchronous layout:

// 较差:读写交替,可能反复触发布局
for (const item of items) {
  item.style.width = `${container.offsetWidth}px`
}

// 较好:先读取,再集中写入
const width = container.offsetWidth
for (const item of items) {
  item.style.width = `${width}px`
}

offsetWidthscrollTopgetBoundingClientRect() 等读取要结合当前布局脏状态判断。box-sizing: border-box 方便尺寸计算,但不会让布局免费,也不能单独解决回流问题。

可以使用 requestAnimationFrame 把视觉写入安排到渲染帧附近,但不要把所有逻辑机械塞进 rAF:

let pending = false

function updatePosition() {
  if (pending) return
  pending = true

  requestAnimationFrame(() => {
    pending = false
    track.style.transform = `translateX(${offset}px)`
  })
}

5. 非可见内容

.long-section {
  content-visibility: auto;
  contain-intrinsic-size: 800px;
}

content-visibility: auto 可以让浏览器跳过远离视口内容的部分渲染工作,但要为滚动高度提供合理的估算,并测试查找、打印、辅助技术和动态内容。虚拟列表、分页和折叠内容也应根据实际 UX 使用。

八、从网络到渲染的实用优化清单

HTML 和资源加载

  • 服务器尽早返回 HTML,减少 TTFB 和不必要重定向;
  • 为关键 CSS、字体、LCP 图片规划加载优先级;
  • 非首屏图片使用 loading="lazy",首屏图片不要误用 lazy;
  • 使用 srcsetsizes 发送合适尺寸的图片;
  • 给图片写宽高或使用 aspect-ratio,减少 CLS;
  • preconnect 只用于少量关键跨源,preload 要匹配最终 as、类型和 CORS;
  • 使用 fetchpriority 只调整确实重要的资源,不要给所有请求 high。
<img
  src="/images/hero-1280.avif"
  srcset="/images/hero-640.avif 640w, /images/hero-1280.avif 1280w"
  sizes="(max-width: 768px) 100vw, 1200px"
  width="1200"
  height="675"
  alt="产品介绍"
  fetchpriority="high"
>

CSS、JavaScript 和第三方脚本

  • 删除未使用 CSS 和重复依赖;
  • 通过 defer、模块、路由级代码分割减少阻塞;
  • 降低水合范围,必要时使用 islands、部分水合或静态 HTML;
  • 对第三方脚本设置加载时机、超时、权限和预算;
  • 把大计算拆成小任务,避免单个任务长期占用主线程;
  • 使用 Worker 处理适合并行的解析和计算,但注意消息复制、序列化和内存成本;
  • 使用 DevTools Performance、Coverage、Network 和 Lighthouse 找到真正瓶颈。

缓存和部署

  • 内容指纹静态资源并设置长缓存;
  • HTML 使用协商缓存,确保新版本能获取新的资源 URL;
  • 压缩文本,正确设置 Vary: Accept-Encoding
  • CDN 缓存公共资源,个性化响应使用 privateno-store
  • Service Worker 采用明确的版本和更新策略;
  • source map、调试接口和管理端资源不能泄露敏感信息。

九、SPA、MPA、SSR 和混合架构

1. 纯 SPA

优点:

  1. 路由切换可以复用已加载的应用和状态;
  2. 适合交互密集、登录后使用、前后端分离的应用;
  3. 可以按路由和组件懒加载。

缺点:

  1. 首次加载需要下载、解析和执行 JavaScript,并可能进行水合或渲染;
  2. 首屏数据和应用初始化可能造成空白或 loading;
  3. SEO、分享预览和无 JavaScript 降级需要额外设计;
  4. 长时间运行的状态和事件监听可能产生内存问题。

2. 纯 MPA

优点:

  1. 服务端可以直接输出每个页面的 HTML;
  2. 内容首屏和 SEO 通常更直接;
  3. 每个页面边界清晰。

缺点:

  1. 跨页面导航可能重新加载文档和公共资源;
  2. 需要处理页面间状态、过渡和重复模板;
  3. 通过把所有页面资源合成一个大文件并不一定更快。

3. SSR、SSG、ISR 和渐进增强

现代框架可以组合:

  • SSR:请求时生成 HTML,适合动态内容;
  • SSG:构建时生成 HTML,适合内容站;
  • ISR/增量生成:按时间或事件更新部分页面;
  • 流式 SSR:逐步发送可用 HTML,但数据依赖和错误边界更复杂;
  • 客户端增强:先提供可读 HTML,再加载交互脚本。

原文提到的“站内切换返回 JS/CSS/HTML,刷新时服务端渲染”可以理解为混合架构思路。现在应由框架的路由、数据加载、预取和缓存机制实现,不要自行根据一个简单的 URL 判断就返回不完整的页面。

十、性能预算和持续回归

可以按页面类型制定预算:

首页:LCP p75 <= 2.5s,CLS p75 <= 0.1
初始 JS:不超过团队约定大小
关键请求:不超过团队约定数量
长任务:限制 >50ms 任务数量和总阻塞时间
图片:每个断点使用合适尺寸,禁止无意发送原图

预算不是越小越好,而是让新增功能显式承担成本。CI 可以检查构建产物大小和 Lighthouse 实验室指标;线上 RUM 用版本维度比较 p75/p95;出现回归时结合资源、长任务和错误详情定位。

十一、原文中的错误和过时结论

  • “网页请求并发上限一般固定为五个”已过时;浏览器连接和流管理受协议、来源、代理和实现影响;
  • CDN 不只是为了突破连接数,也不是自动去 Cookie 的万能手段;
  • Brotli 对图片压缩效果好不准确,Brotli/Gzip 主要用于文本;
  • Expires 拼写应正确,缓存没有固定的 cache-control > ecprise > etag 总优先级表;
  • 同步经典脚本会阻塞 HTML 解析,不能说“只影响渲染不影响解析”;
  • CSS 通常阻塞首次渲染,CSS 后的脚本可能等待样式表,但 CSS 不会在所有情况下阻止 HTML 解析;
  • “所有 position、overflow 都独立成层”“使用 transform 就跳过重排重绘”都过于绝对;
  • 读取 offset 等属性可能强制布局,但不是每次读取都必然回流;
  • translateZ(0)gpu.js 和把所有内容放进 GPU 不是通用性能方案;
  • localStorage 不适合保存大体积资源,现代离线缓存优先考虑 Cache API 和 IndexedDB;
  • SPA、MPA、SSR 各有取舍,不能把某一种架构当作所有页面的最优解。

总结

  1. 性能优化必须以测量和用户体验为中心,而不是机械背规则;
  2. HTTP/2/3 改变了请求复用方式,但减少关键字节、合理缓存和资源优先级仍然重要;
  3. Brotli/Gzip 主要压缩文本,图片要从格式、尺寸、质量和响应式资源优化;
  4. 现代渲染包括样式、布局、绘制、栅格化和合成,GPU 加速不是万能开关;
  5. 通过 defer、模块、代码分割、懒加载、srcset、缓存和第三方脚本治理降低关键路径成本;
  6. Service Worker/Cache API 比 localStorage 更适合离线资源,仍需版本、更新和隐私策略;
  7. SPA、MPA、SSR、SSG 和混合架构应按业务、SEO、交互、缓存和部署成本选择;
  8. 用实验室指标发现问题,用 RUM 验证真实收益,并持续进行性能预算回归。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS