浏览器页面加载完整流程

1. DNS解析

浏览器将域名解析为IP地址。

https://www.example.com

DNS查询

返回IP: 192.168.1.1

优化策略:

<!-- DNS预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://api.example.com" />

2. TCP连接

浏览器与服务器建立TCP连接(三次握手)。

SYN →
  ← SYN-ACK
ACK →

优化策略:

<!-- TCP预连接 -->
<link rel="preconnect" href="https://cdn.example.com" />

3. HTTP请求

浏览器发送HTTP请求,获取HTML文档。

GET /index.html HTTP/1.1
Host: www.example.com

4. HTML解析与DOM构建

浏览器解析HTML文档,构建DOM树。

HTML

解析器

DOM树

DOM树示例:

<!DOCTYPE html>
<html>
  <head>
    <title>页面标题</title>
  </head>
  <body>
    <div class="container">
      <h1>标题</h1>
      <p>段落</p>
    </div>
  </body>
</html>
// DOM树结构
DOM {
  type: 'html',
  children: [
    { type: 'head', children: [{ type: 'title', text: '页面标题' }] },
    {
      type: 'body'
      children: [
        {
          type: 'div',
          class: 'container',
          children: [
            { type: 'h1', text: '标题' },
            { type: 'p', text: '段落' }
          ]
        }
      ]
    }
  ]
}

5. CSS解析与CSSOM构建

浏览器解析CSS,构建CSSOM(CSS对象模型)树。

.container {
  width: 100%;
  padding: 20px;
}

h1 {
  font-size: 24px;
  color: #333;
}
// CSSOM树结构
CSSOM {
  '.container': {
    width: '100%',
    padding: '20px'
  },
  'h1': {
    'font-size': '24px',
    color: '#333'
  }
}

6. JavaScript执行

浏览器解析和执行JavaScript代码。

7. 渲染树构建

结合DOM树和CSSOM树,构建渲染树。

DOM树 + CSSOM树

    渲染树

8. 布局与绘制

浏览器计算元素位置和大小(布局),然后绘制到屏幕上。

CSS的加载与阻塞机制

1. CSS阻塞渲染

CSS文件会阻塞页面的渲染(不阻塞解析):

<!DOCTYPE html>
<html>
  <head>
    <!-- 这个CSS会阻塞渲染 -->
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>标题</h1>
    <!-- 在CSS加载完成前,用户看不到任何内容 -->
  </body>
</html>

原因:

  • 浏览器为了避免”FOUC”(Flash of Unstyled Content,无样式内容闪烁)
  • 等待CSSOM构建完成后才能构建渲染树

2. CSS不阻塞DOM解析

<!DOCTYPE html>
<html>
  <head>
    <!-- CSS加载时,DOM解析继续进行 -->
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>标题</h1>
    <p>段落</p>
    <!-- DOM树会继续构建,但渲染会等待CSS -->
  </body>
</html>

3. CSS异步加载(不推荐)

<!-- 非阻塞CSS,可能引起样式闪烁 -->
<link rel="stylesheet" href="print.css" media="print" />

<!-- JavaScript加载的CSS -->
<script>
  const link = document.createElement('link')
  link.rel = 'stylesheet'
  link.href = 'async-styles.css'
  document.head.appendChild(link)
</script>

警告:

  • 会引起”FOUC”问题
  • 影响用户体验
  • 仅适用于非关键样式(如打印样式)

JavaScript的加载与阻塞机制

1. 默认行为:阻塞解析和渲染

<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <h1>标题</h1>

    <!-- 默认script会阻塞DOM解析和渲染 -->
    <script src="script.js"></script>

    <p>段落</p>
    <!-- 这个元素要等script执行完才能被解析 -->
  </body>
</html>

执行顺序:

  1. 解析到<script>标签
  2. 停止DOM解析
  3. 下载并执行JavaScript
  4. 继续解析剩余HTML

2. async:异步加载,不保证顺序

<!-- async: 异步加载,不阻塞解析 -->
<script async src="script1.js"></script>
<script async src="script2.js"></script>

特点:

  • 异步下载,不阻塞DOM解析
  • 下载完成后立即执行(可能阻塞渲染)
  • 不保证执行顺序(先下载完的先执行)

使用场景:

  • 独立的第三方脚本(如Google Analytics)
  • 不依赖其他脚本
  • 不依赖DOM结构
<!-- 推荐:Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>

3. defer:异步加载,保证顺序

<!-- defer: 异步加载,DOM解析完成后执行 -->
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>

特点:

  • 异步下载,不阻塞DOM解析
  • DOMContentLoaded之前执行
  • 保证执行顺序(按HTML中的顺序执行)
  • 可以访问完整的DOM

使用场景:

  • DOM操作脚本
  • 依赖其他脚本
  • 需要访问DOM元素
<!-- 推荐:应用主脚本 -->
<script defer src="app.js"></script>
<script defer src="components.js"></script>

4. 三种方式对比

特性默认asyncdefer
加载时机同步异步异步
阻塞DOM解析✅ 是❌ 否❌ 否
执行时机立即执行下载完立即DOM解析完成
执行顺序按顺序不保证按顺序
可访问DOM部分访问部分访问完整访问
适用于关键脚本独立第三方应用主脚本

实际测试案例

案例1:默认script阻塞

<!DOCTYPE html>
<html>
  <body>
    <h1>测试页面</h1>

    <script>
      // 模拟耗时操作
      const start = Date.now()
      while (Date.now() - start < 2000) {
        // 阻塞2秒
      }
      console.log('script执行完成')
    </script>

    <p>这个段落要等2秒后才能显示</p>
  </body>
</html>

结果: 页面白屏2秒,然后显示所有内容。

案例2:async不保证顺序

<!DOCTYPE html>
<html>
  <body>
    <script async>
      // 模拟网络延迟
      setTimeout(() => {
        console.log('script1执行')
      }, 1000)
    </script>

    <script async>
      console.log('script2执行')
    </script>
  </body>
</html>

输出:

script2执行
script1执行

案例3:defer保证顺序

<!DOCTYPE html>
<html>
  <body>
    <script defer>
      setTimeout(() => {
        console.log('script1执行')
      }, 1000)
    </script>

    <script defer>
      console.log('script2执行')
    </script>

    <script>
      document.addEventListener('DOMContentLoaded', () => {
        console.log('DOMContentLoaded触发')
      })
    </script>
  </body>
</html>

输出:

script1执行
script2执行
DOMContentLoaded触发

最佳实践

1. CSS优化

<!DOCTYPE html>
<html>
  <head>
    <!-- 1. 关键CSS内联(First Paint) -->
    <style>
      /* 首屏关键样式 */
      body {
        margin: 0;
        padding: 0;
        font-family: system-ui;
      }
      .header {
        height: 60px;
        background: #333;
      }
    </style>

    <!-- 2. 关键CSS文件优先加载 -->
    <link rel="stylesheet" href="critical.css" />

    <!-- 3. 预加载其他CSS -->
    <link rel="preload" href="main.css" as="style" />
    <link rel="preload" href="components.css" as="style" />

    <!-- 4. 媒体查询加载(条件加载) -->
    <link rel="stylesheet" href="print.css" media="print" />
    <link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />

    <!-- 5. 异步加载非关键CSS -->
    <script>
      // 延迟加载动画CSS
      setTimeout(() => {
        const link = document.createElement('link')
        link.rel = 'stylesheet'
        link.href = 'animations.css'
        document.head.appendChild(link)
      }, 1000)
    </script>
  </head>
  <body>
    <!-- 页面内容 -->
  </body>
</html>

2. JavaScript优化

<!DOCTYPE html>
<html>
  <head>
    <!-- 1. 预连接 -->
    <link rel="preconnect" href="https://cdn.example.com" />

    <!-- 2. 预加载关键脚本 -->
    <link rel="preload" href="critical.js" as="script" />

    <!-- 3. 关键脚本使用defer或内联 -->
    <script defer src="critical.js"></script>

    <!-- 4. 内联关键脚本(极小) -->
    <script>
      // 防止FOUC(可选)
      document.documentElement.style.visibility = 'hidden'
      window.addEventListener('load', () => {
        document.documentElement.style.visibility = ''
      })
    </script>
  </head>
  <body>
    <!-- 页面内容 -->

    <!-- 5. 应用主脚本使用defer -->
    <script defer src="app.js"></script>
    <script defer src="components.js"></script>

    <!-- 6. 第三方脚本使用async -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>

    <!-- 7. 非关键脚本延迟加载 -->
    <script>
      window.addEventListener('load', () => {
        const script = document.createElement('script')
        script.src = 'non-critical.js'
        document.body.appendChild(script)
      })
    </script>

    <!-- 8. 或使用defer + 延迟执行 -->
    <script defer>
      document.addEventListener('DOMContentLoaded', () => {
        // 延迟执行非关键功能
        setTimeout(() => {
          initAnalytics()
          initChatWidget()
        }, 2000)
      })
    </script>
  </body>
</html>

3. 脚本位置建议

<!DOCTYPE html>
<html>
  <head>
    <!-- CSS -->
    <link rel="stylesheet" href="styles.css" />

    <!-- 预加载资源 -->
    <link rel="preload" href="font.woff2" as="font" crossorigin />
    <link rel="preload" href="hero-image.jpg" as="image" />

    <!-- 关键脚本(defer) -->
    <script defer src="polyfills.js"></script>
    <script defer src="app.js"></script>
  </head>
  <body>
    <!-- 页面内容 -->

    <!-- 第三方脚本(async) -->
    <script async src="analytics.js"></script>

    <!-- 懒加载脚本 -->
    <script>
      function loadScript(src) {
        return new Promise(resolve => {
          const script = document.createElement('script')
          script.src = src
          script.onload = resolve
          document.head.appendChild(script)
        })
      }

      // 用户交互时加载
      document.querySelector('.chat-button').addEventListener(
        'click',
        async () => {
          await loadScript('chat-widget.js')
          initChat()
        },
        { once: true }
      )
    </script>
  </body>
</html>

性能指标与监测

1. 关键性能指标

// 监测关键性能指标
window.addEventListener('load', () => {
  const timing = performance.timing

  console.log({
    // DNS查询时间
    dns: timing.domainLookupEnd - timing.domainLookupStart,

    // TCP连接时间
    tcp: timing.connectEnd - timing.connectStart,

    // 请求时间
    request: timing.responseEnd - timing.requestStart,

    // DOM解析时间
    domParse: timing.domComplete - timing.domLoading,

    // 资源加载时间
    resourceLoad: timing.loadEventEnd - timing.domComplete,

    // 总页面加载时间
    totalLoad: timing.loadEventEnd - timing.navigationStart,

    // TTFB (Time to First Byte)
    ttfb: timing.responseStart - timing.requestStart,
  })
})

2. Core Web Vitals

// 监测LCP (Largest Contentful Paint)
import { onLCP } from 'web-vitals'

onLCP(metric => {
  console.log('LCP:', metric.value)
  // LCP < 2.5s 良好
  // LCP 2.5s - 4.0s 需要改进
  // LCP > 4.0s 差
})

// 监测CLS (Cumulative Layout Shift)
import { onCLS } from 'web-vitals'

onCLS(metric => {
  console.log('CLS:', metric.value)
  // CLS < 0.1 良好
  // CLS 0.1 - 0.25 需要改进
  // CLS > 0.25 差
})

常见问题

Q: 为什么建议把<script>放在</body>之前?

A:

  • 历史原因:避免阻塞DOM解析
  • 现代做法:使用deferasync,可以放在<head>
  • 优点:资源并行加载,提前开始下载
<!-- 传统做法 -->
<body>
  <!-- 内容 -->
  <script src="app.js"></script>
</body>

<!-- 现代做法 -->
<head>
  <script defer src="app.js"></script>
</head>

Q: asyncdefer可以一起使用吗?

A: 不可以。浏览器会忽略defer,只执行async

<!-- 这等同于 async -->
<script async defer src="script.js"></script>

Q: 动态插入的script是async还是defer?

A: 默认是async

// 动态插入的脚本默认async
const script = document.createElement('script')
script.src = 'dynamic.js'
document.head.appendChild(script)

// 如果需要defer行为
script.defer = true

Q: 如何处理模块脚本?

A: 使用type="module",默认defer

<!-- 模块脚本自动defer -->
<script type="module" src="app.js"></script>

<!-- 模块脚本无法使用async(因为自动defer) -->
<!-- <script type="module" async src="app.js"></script> -->

<!-- 动态导入模块 -->
<script>
  import('./heavy-module.js').then(module => {
    module.init()
  })
</script>

Q: CSS加载会阻塞script执行吗?

A: 不会,除非script依赖CSS。

<link rel="stylesheet" href="styles.css" />

<script>
  // 这个脚本会立即执行,不等待CSS
  console.log('script执行')
</script>

<script>
  // 这个脚本会等待CSS(因为访问了样式)
  const el = document.getElementById('header')
  const width = el.offsetWidth // 触发布局,等待CSS
  console.log('宽度:', width)
</script>

现代优化策略

1. 资源提示

<!-- DNS预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com" />

<!-- TCP预连接 -->
<link rel="preconnect" href="https://cdn.example.com" />

<!-- 预加载资源 -->
<link rel="preload" href="font.woff2" as="font" crossorigin />
<link rel="preload" href="image.jpg" as="image" />

<!-- 预获取(低优先级) -->
<link rel="prefetch" href="next-page.html" />
<link rel="prefetch" href="lazy-component.js" />

2. Service Worker缓存

// 缓存关键资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll(['/', '/styles.css', '/app.js', '/favicon.ico'])
    })
  )
})

// 拦截请求
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request)
    })
  )
})

3. HTTP/2/3多路复用

  • 同一域名下多个资源并行加载
  • 无需手动合并文件
  • 支持服务器推送

总结

页面加载流程关键点

  1. DNS解析 → 优化:dns-prefetch
  2. TCP连接 → 优化:preconnect
  3. HTML解析 → 构建DOM树
  4. CSS解析 → 构建CSSOM树(阻塞渲染)
  5. JavaScript执行 → 优化:async/defer
  6. 渲染树构建 → DOM + CSSOM
  7. 布局与绘制 → 显示页面

CSS加载优化

  • ✅ 关键CSS内联
  • ✅ 使用<link>加载
  • ✅ 预加载非关键CSS
  • ❌ 避免JavaScript异步加载CSS

JavaScript加载优化

  • ✅ 应用脚本使用defer
  • ✅ 第三方脚本使用async
  • ✅ 模块脚本自动defer
  • ✅ 动态导入懒加载
  • ❌ 避免阻塞式脚本

性能优化黄金法则

  1. 减少HTTP请求数(HTTP/1.1)
  2. 使用CDN加速
  3. 启用Gzip压缩
  4. 优化图片和字体
  5. 利用浏览器缓存
  6. 使用现代加载策略

参考资源