在浏览器渲染页面的过程中,HTML解析、CSSOM构建、JavaScript执行和DOM渲染之间存在着复杂的依赖关系。理解这些关系是优化性能的第一步。
关键概念:
CSS被认为是渲染阻塞资源,因为:
<link rel="stylesheet">会阻塞渲染,直到样式表下载并解析完成<!-- 传统方式:会阻塞渲染 -->
<link rel="stylesheet" href="styles.css">
<!-- 优化方式:使用媒体查询避免非必要阻塞 -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)">将首屏渲染所需的关键CSS直接内联到HTML中,避免额外的HTTP请求。
<head>
<style>
/* 关键CSS:首屏可见内容所需样式 */
.header, .hero, .navigation {
/* 精简的关键样式 */
}
</style>
<!-- 非关键CSS异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
</head>使用媒体查询让浏览器仅对匹配当前设备的样式表阻塞渲染。
<!-- 仅对屏幕且宽度大于1200px的设备阻塞渲染 -->
<link rel="stylesheet" href="desktop.css" media="screen and (min-width: 1200px)">
<!-- 移动端样式,仅对匹配的设备阻塞 -->
<link rel="stylesheet" href="mobile.css" media="screen and (max-width: 768px)">使用preload提前加载重要CSS资源。
<head>
<!-- 高优先级加载关键CSS -->
<link rel="preload" href="critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="critical.css"></noscript>
<!-- 低优先级加载非关键CSS -->
<link rel="preload" href="non-critical.css" as="style" media="print" onload="this.media='all'">
</head>现代构建工具支持CSS代码分割:
// Webpack配置示例
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
}
};
// 动态导入CSS(适用于现代框架)
import('./dynamic-styles.css').then(() => {
// 样式加载完成后的回调
});JavaScript可以阻塞DOM构建和渲染,原因包括:
<script>标签时会暂停DOM构建,直到脚本下载并执行完成将非关键脚本移到页面底部。
<!-- 不推荐的写法:在head中加载阻塞脚本 -->
<head>
<script src="blocking-script.js"></script>
</head>
<!-- 推荐的写法:非关键脚本放在body末尾 -->
<body>
<!-- 页面内容 -->
<script src="non-critical-script.js" defer></script>
</body><!-- defer:HTML解析完成后按顺序执行 -->
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
<!-- async:下载完成后立即执行,不保证顺序 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
<!-- module类型默认具有defer行为 -->
<script type="module" src="app.js"></script>async vs defer对比表:
特性 | <script> | <script async> | <script defer> |
|---|---|---|---|
阻塞HTML解析 | 是 | 否 | 否 |
执行时机 | 立即执行 | 下载完成后立即执行 | HTML解析完成后执行 |
执行顺序 | 顺序执行 | 无序执行 | 顺序执行 |
适用场景 | 极少使用 | 独立第三方脚本 | 依赖DOM的脚本 |
// 动态导入(ES2020+)
const loadModule = async () => {
const module = await import('./module.js');
module.init();
};
// 基于路由的代码分割(React示例)
const HomePage = React.lazy(() => import('./HomePage'));
const AboutPage = React.lazy(() => import('./AboutPage'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Router>
<Route path="/" component={HomePage} />
<Route path="/about" component={AboutPage} />
</Router>
</Suspense>
);
}将计算密集型任务移至Web Worker,避免阻塞主线程。
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = (event) => {
const result = processData(event.data);
self.postMessage(result);
};requestIdleCallback调度非关键任务// 使用requestIdleCallback执行低优先级任务
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
performTask(tasks.pop());
}
if (tasks.length > 0) {
requestIdleCallback(processTasks);
}
});<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="critical.js" as="script">
<!-- 2. 内联关键CSS -->
<style>
/* 精简的关键CSS */
</style>
<!-- 3. 异步加载非关键CSS -->
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
<!-- 4. 预连接重要源 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
</head>
<body>
<!-- 首屏内容 -->
<!-- 5. 异步加载非关键JS -->
<script>
// 内联脚本加载非关键JS
function loadScript(src) {
const script = document.createElement('script');
script.src = src;
script.async = true;
document.body.appendChild(script);
}
// 延迟加载非关键脚本
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
loadScript('non-critical.js');
});
} else {
setTimeout(() => {
loadScript('non-critical.js');
}, 3000);
}
</script>
<!-- 6. 关键JS使用defer -->
<script src="critical.js" defer></script>
</body>
</html>// vite.config.js (Vite示例)
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'moment']
}
}
},
cssCodeSplit: true,
reportCompressedSize: false
}
};
// webpack.config.js (Webpack示例)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};// 使用Performance API监控渲染性能
function measurePerformance() {
const timing = performance.getEntriesByType('navigation')[0];
console.log('关键指标:');
console.log('DOMContentLoaded:', timing.domContentLoadedEventEnd);
console.log('首次绘制(FP):', performance.getEntriesByName('first-paint')[0].startTime);
console.log('首次内容绘制(FCP):', performance.getEntriesByName('first-contentful-paint')[0].startTime);
// 监听长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('长任务:', entry);
}
});
observer.observe({ entryTypes: ['longtask'] });
}
// 检测阻塞时间
function measureBlockingTime() {
const blockingTime = performance.now() - performance.timing.navigationStart;
console.log(`总阻塞时间: ${blockingTime}ms`);
}优化前:
优化措施:
defer加载非关键JS优化后:
defer和async避免JS和CSS对DOM渲染的阻塞是一个系统工程,需要从资源加载策略、代码结构优化、构建配置和运行监控多个层面入手。通过理解浏览器渲染机制,合理使用现代Web平台的各项功能,我们可以显著提升页面加载性能,改善用户体验。
记住,没有一种优化方案适合所有场景,最佳实践是根据实际应用的特点,测量、分析、优化,再测量,形成持续优化的闭环。