浏览器页面加载完整流程
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>
执行顺序:
- 解析到
<script>标签 - 停止DOM解析
- 下载并执行JavaScript
- 继续解析剩余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. 三种方式对比
| 特性 | 默认 | async | defer |
|---|---|---|---|
| 加载时机 | 同步 | 异步 | 异步 |
| 阻塞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解析
- 现代做法:使用
defer或async,可以放在<head>中 - 优点:资源并行加载,提前开始下载
<!-- 传统做法 -->
<body>
<!-- 内容 -->
<script src="app.js"></script>
</body>
<!-- 现代做法 -->
<head>
<script defer src="app.js"></script>
</head>
Q: async和defer可以一起使用吗?
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多路复用
- 同一域名下多个资源并行加载
- 无需手动合并文件
- 支持服务器推送
总结
页面加载流程关键点
- DNS解析 → 优化:dns-prefetch
- TCP连接 → 优化:preconnect
- HTML解析 → 构建DOM树
- CSS解析 → 构建CSSOM树(阻塞渲染)
- JavaScript执行 → 优化:async/defer
- 渲染树构建 → DOM + CSSOM
- 布局与绘制 → 显示页面
CSS加载优化
- ✅ 关键CSS内联
- ✅ 使用
<link>加载 - ✅ 预加载非关键CSS
- ❌ 避免JavaScript异步加载CSS
JavaScript加载优化
- ✅ 应用脚本使用
defer - ✅ 第三方脚本使用
async - ✅ 模块脚本自动
defer - ✅ 动态导入懒加载
- ❌ 避免阻塞式脚本
性能优化黄金法则
- 减少HTTP请求数(HTTP/1.1)
- 使用CDN加速
- 启用Gzip压缩
- 优化图片和字体
- 利用浏览器缓存
- 使用现代加载策略