从输入 URL 到页面渲染完毕,中间发生了什么?以及在每个环节可以做哪些优化。核心原则:空间换时间。

从输入 URL 到渲染

加载阶段

  1. DNS 解析:域名 → IP 地址。浏览器缓存 → OS 缓存 → 路由器缓存 → ISP DNS 服务器,逐级查询
  2. TCP 连接:三次握手建立连接。HTTPS 额外进行 TLS 握手
  3. HTTP 请求:浏览器发送请求,服务器处理并返回响应

渲染阶段

  1. 解析 HTML,构建 DOM Tree
  2. 解析 CSS,构建 CSSOM(CSS Object Model)
  3. 将 DOM Tree 和 CSSOM 合并为 Render Tree(只包含可见元素,display: none 不在其中)
  4. Layout(布局/回流):计算每个节点的几何位置和尺寸
  5. Paint(绘制):将每个节点绘制到屏幕上
  6. Composite(合成):将不同图层合并显示
HTML ──→ DOM Tree ──┐
                     ├──→ Render Tree → Layout → Paint → Composite
CSS  ──→ CSSOM  ────┘

<script> 标签会阻塞 HTML 解析(除非有 asyncdefer)。CSS 不会阻塞 HTML 解析但会阻塞渲染——在 CSSOM 构建完成之前,浏览器不会渲染任何内容。

async vs defer

<!-- 普通 script:阻塞 HTML 解析,立即下载并执行 -->
<script src="main.js"></script>
 
<!-- async:异步下载,下载完立即执行(执行时阻塞解析),适合独立脚本 -->
<script src="analytics.js" async></script>
 
<!-- defer:异步下载,HTML 解析完才执行,保证执行顺序 -->
<script src="app.js" defer></script>
属性下载执行时机执行顺序
阻塞解析下载后立即执行按文档顺序
async异步下载完立即执行不保证顺序(谁先下载完谁先执行)
defer异步DOM 解析完后、DOMContentLoaded按文档顺序

DOMContentLoaded vs load

// DOM 解析完毕(CSS、图片可能尚未加载完)
window.addEventListener('DOMContentLoaded', () => {
  // 可以开始操作 DOM,初始化交互
});
 
// 全部资源(图片、CSS、iframe)加载完
window.addEventListener('load', () => {
  // 可以获取图片尺寸等信息
});
  • DOMContentLoaded → HTML 解析完,但样式表、图片、子框架可能尚未加载
  • load → 所有资源加载完毕
  • CSS 样式表会阻塞 DOMContentLoaded(因为 JS 可能需要读取样式信息)

性能优化

核心原则:多利用内存和缓存,减少 CPU 计算和网络加载耗时。

让加载更快

方向具体手段
减少资源体积代码压缩(Terser、cssnano)、Gzip/Brotli 压缩、图片压缩(WebP/AVIF)、Tree Shaking
减少请求数代码合并(webpack splitChunks)、SSR 服务端渲染、雪碧图/icon font/SVG sprite
更快网络CDN 就近服务、HTTP/2 多路复用、DNS 预解析(<link rel="dns-prefetch">
预加载<link rel="preload"> 提前加载关键资源、<link rel="prefetch"> 预加载下一页资源

资源文件名带 hash

// webpack
module.exports = {
  mode: 'production',
  output: {
    filename: 'bundle.[contenthash:8].js', // 8 位 hash
  },
};

文件内容不变 → hash 不变 → URL 不变 → 浏览器走 HTTP 缓存(304)。配合 Cache-Control: max-age=31536000, immutable 使用效果最佳。

CDN

  • 静态资源部署到 CDN
  • 根据用户地理位置就近服务,降低延迟
  • 配合 HTTP 缓存和内容 hash 实现”永久”缓存

SSR

方式流程首屏速度代表
SSR服务端渲染 HTML,数据和页面一起返回快(首屏直接显示)Next.js、Nuxt.js
前后端分离先加载空壳 HTML → 请求数据 → 渲染慢(需要等待 JS 执行)传统 SPA

SSR 加速的是首屏可见的时间(FCP / LCP),但不是 TTI(Time to Interactive)——页面可见后仍需要加载并执行 JS 才能交互。

资源提示(Resource Hints)

<!-- DNS 预解析:提前解析域名 -->
<link rel="dns-prefetch" href="https://api.example.com" />
 
<!-- 预连接:DNS + TCP + TLS 全部提前建立 -->
<link rel="preconnect" href="https://api.example.com" />
 
<!-- 预加载:当前页面需要的关键资源,优先级高 -->
<link rel="preload" href="/font.woff2" as="font" crossorigin />
 
<!-- 预获取:下个页面可能用到的资源,空闲时下载,优先级低 -->
<link rel="prefetch" href="/next-page.js" />

让渲染更快

方向具体手段
CSS 在 <head>,JS 在 </body>避免 JS 阻塞 HTML 解析
尽早触发非关键 JSDOMContentLoadeddefer
懒加载图片 <img loading="lazy">、滚动加载更多、路由懒加载
缓存 DOM 查询减少重复读取布局属性
批量 DOM 操作DocumentFragmentdisplay: none 容器
防抖与节流控制 scroll、resize、input 等高频事件
will-change提前告知浏览器哪些属性会变化,创建独立图层
虚拟列表只渲染可视区域的 DOM 节点(大量数据列表场景)

图片懒加载

<!-- 原生懒加载 -->
<img src="real.png" loading="lazy" alt="" />
 
<!-- JS 手动实现 -->
<img src="placeholder.png" data-src="real.png" alt="" />
<script>
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img);
      }
    });
  });
  document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));
</script>

相关笔记