【前端面试小册】浏览器-第11节:JS与CSS加载顺序深度解析,彻底搞懂阻塞与性能优化
一、现实世界类比 🏗️
想象浏览器渲染页面就像盖房子:
- HTML:就像建筑图纸
- 边看图纸边盖房(边下载边解析)
- CSS:就像装修材料
- 不影响看图纸(不阻塞DOM解析)
- 但装修完才能入住(阻塞页面渲染)
- JavaScript:就像监理
- 监理来了要停工(阻塞DOM解析)
- 监理需要看装修效果(依赖CSS)
- 监理走了才能继续盖(JS执行完才能继续)
关键理解:JS 全阻塞,CSS 半阻塞!
二、核心概念:阻塞 vs 非阻塞
📊 阻塞情况总结
const BlockingSummary = {
HTML: {
解析: '边下载边解析',
阻塞: '不会被阻塞(除非遇到 script)'
},
JavaScript: {
解析: '同步执行',
阻塞: {
'阻塞DOM解析': '✅ 是',
'阻塞DOM渲染': '✅ 是',
'阻塞资源加载': '⚠️ 部分(阻塞后续资源)'
},
原因: 'JS 可能修改 DOM(document.write),必须等待执行完'
},
CSS: {
解析: '异步下载',
阻塞: {
'阻塞DOM解析': '❌ 否',
'阻塞DOM渲染': '✅ 是',
'阻塞JS执行': '✅ 是'
},
原因: 'JS 可能读取样式(getComputedStyle),必须等待 CSS 加载完'
}
};
三、JavaScript 的加载与执行
🔴 普通 Script(默认行为)
<!--===== 普通 script 标签 =====-->
<!DOCTYPE html>
<html>
<head>
<title>Script Loading</title>
</head>
<body>
<h1>开始</h1>
<!-- ❌ 阻塞 DOM 解析 -->
<script src="heavy.js"></script>
<h1>结束</h1> <!-- 要等 heavy.js 下载并执行完才能解析 -->
</body>
</html>
<!--
执行流程:
1. 解析 HTML → 遇到 <h1>开始</h1>
2. 解析 HTML → 遇到 <script>
3. ⏸️ 暂停 HTML 解析
4. 下载 heavy.js
5. 执行 heavy.js
6. ✅ 恢复 HTML 解析
7. 解析 <h1>结束</h1>
问题:
- 如果 heavy.js 很大(5MB),页面会卡住 5 秒
- 用户看到空白页面(白屏时间长)❌
-->
// ===== heavy.js(模拟大文件)=====
console.log('开始执行 JS');
// 模拟复杂计算(阻塞 3 秒)
const start = Date.now();
while (Date.now() - start < 3000) {
// 死循环 3 秒
}
console.log('JS 执行完毕');
// 结果:页面卡住 3 秒,用户体验极差 ❌
🟢 async(异步执行)⭐⭐⭐⭐
<!--===== async 属性 =====-->
<!DOCTYPE html>
<html>
<head>
<title>Async Script</title>
</head>
<body>
<h1>开始</h1>
<!-- ✅ 不阻塞 DOM 解析,下载后立即执行 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
<h1>结束</h1>
</body>
</html>
<!--
执行流程:
1. 解析 HTML → 遇到 <h1>开始</h1>
2. 解析 HTML → 遇到 async script
3. ✅ 不暂停,继续解析 HTML(同时后台下载 JS)
4. 解析 <h1>结束</h1>
5. analytics.js 下载完成 → ⏸️ 暂停 HTML 解析 → 执行 JS
6. ads.js 下载完成 → ⏸️ 暂停 HTML 解析 → 执行 JS
特点:
✅ 不阻塞 DOM 解析(下载时)
❌ 执行时会阻塞(下载完立即执行)
❌ 不保证顺序(谁先下载完谁先执行)
-->
// ===== 适用场景 =====
const AsyncUseCases = {
适合: [
'统计脚本(Google Analytics)',
'广告脚本',
'独立的工具库',
'不依赖其他脚本的代码'
],
不适合: [
'依赖DOM的脚本(需要等待DOM加载完成)',
'有依赖关系的脚本(jQuery → jQuery插件)',
'需要按顺序执行的脚本'
]
};
// ===== 例子:Google Analytics =====
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
// ✅ 不影响页面加载,统计脚本后台运行
🟡 defer(延迟执行)⭐⭐⭐⭐⭐
<!--===== defer 属性 =====-->
<!DOCTYPE html>
<html>
<head>
<title>Defer Script</title>
</head>
<body>
<h1>开始</h1>
<!-- ✅ 不阻塞 DOM 解析,DOM 解析完后按顺序执行 -->
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>
<h1>结束</h1>
</body>
</html>
<!--
执行流程:
1. 解析 HTML → 遇到 <h1>开始</h1>
2. 解析 HTML → 遇到 defer script
3. ✅ 不暂停,继续解析 HTML(同时后台下载 JS)
4. 解析 <h1>结束</h1>
5. ✅ HTML 解析完成
6. ✅ 按顺序执行:jquery.js → app.js
7. 🎉 触发 DOMContentLoaded 事件
特点:
✅ 不阻塞 DOM 解析
✅ 保证执行顺序
✅ 在 DOMContentLoaded 前执行
✅ 最推荐使用!
-->
// ===== 适用场景 =====
const DeferUseCases = {
适合: [
'所有需要操作 DOM 的脚本',
'有依赖关系的脚本',
'大部分业务代码',
'框架代码(Vue、React)'
],
不适合: [
'内联脚本(defer 只对外部脚本有效)',
'需要立即执行的脚本'
]
};
// ===== 完美的脚本加载策略 =====
<!DOCTYPE html>
<html>
<head>
<title>Best Practice</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app"></div>
<!-- ✅ 所有脚本放在 body 底部,使用 defer -->
<script src="vue.js" defer></script>
<script src="router.js" defer></script>
<script src="app.js" defer></script>
</body>
</html>
// 结果:
// 1. HTML 快速解析完成
// 2. CSS 并行下载
// 3. JS 并行下载
// 4. HTML 解析完后,按顺序执行 JS
// 5. 首屏渲染快 ✅
📊 async vs defer 对比表
| 特性 | 普通 script | async | defer |
|---|---|---|---|
| 下载时阻塞 | ✅ 阻塞 | ❌ 不阻塞 | ❌ 不阻塞 |
| 执行时阻塞 | ✅ 阻塞 | ✅ 阻塞 | ❌ 不阻塞(HTML 解析完后执行) |
| 执行时机 | 立即 | 下载完立即执行 | HTML 解析完后执行 |
| 执行顺序 | ✅ 保证 | ❌ 不保证 | ✅ 保证 |
| DOMContentLoaded | 等待脚本 | 不等待 | 等待脚本 |
| 适用场景 | 无 | 独立脚本 | 所有脚本 |
| 推荐指数 | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
四、CSS 的加载与渲染
💡 CSS 的阻塞行为
<!--===== CSS 加载示例 =====-->
<!DOCTYPE html>
<html>
<head>
<title>CSS Loading</title>
<!-- CSS 不阻塞 DOM 解析 -->
<link rel="stylesheet" href="large.css"> <!-- 5MB -->
</head>
<body>
<h1>标题</h1>
<p>段落</p>
<script>
// ⚠️ 这里会等待 large.css 加载完成
const color = getComputedStyle(document.body).color;
console.log('颜色:', color);
</script>
</body>
</html>
<!--
执行流程:
1. ✅ 解析 HTML(不等待 large.css)
2. ✅ 构建 DOM 树(完成)
3. ⏸️ 等待 large.css 下载完成
4. ✅ 构建 CSSOM 树
5. ✅ 合并 DOM 树 + CSSOM 树 = 渲染树
6. ✅ 渲染页面
关键点:
✅ CSS 不阻塞 DOM 解析
✅ CSS 阻塞页面渲染(必须等待 CSS 加载完)
✅ CSS 阻塞 JS 执行(JS 可能读取样式)
-->
// ===== 为什么 CSS 会阻塞 JS?=====
// 原因:JS 可能读取样式
// 例子1:读取元素宽度
<style>
#box { width: 100px; }
</style>
<div id="box"></div>
<script>
// 如果 CSS 还没加载完,这里会拿到错误的值
const width = document.getElementById('box').offsetWidth;
console.log('宽度:', width); // 必须等待 CSS 加载完
</script>
// 例子2:读取计算样式
<link rel="stylesheet" href="styles.css">
<script>
// 必须等待 styles.css 加载完
const color = getComputedStyle(document.body).backgroundColor;
console.log('背景色:', color);
</script>
// 结论:CSS 必须在 JS 执行前加载完成 ✅
五、最佳实践:性能优化策略
策略1:关键资源优先加载
<!DOCTYPE html>
<html>
<head>
<title>Performance Optimization</title>
<!-- ✅ 1. 关键 CSS 内联(首屏样式)-->
<style>
/* 首屏关键样式 */
body { margin: 0; font-family: sans-serif; }
.header { height: 60px; background: #333; }
.content { padding: 20px; }
</style>
<!-- ✅ 2. 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
</head>
<body>
<!-- ✅ 3. HTML 内容优先 -->
<header class="header">Header</header>
<div class="content">Content</div>
<!-- ✅ 4. 脚本放底部,使用 defer -->
<script src="main.js" defer></script>
</body>
</html>
策略2:preload & prefetch
<!--===== preload:预加载当前页面资源 =====-->
<head>
<!-- ✅ 预加载字体(高优先级)-->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<!-- ✅ 预加载关键脚本 -->
<link rel="preload" href="critical.js" as="script">
<!-- ✅ 预加载关键样式 -->
<link rel="preload" href="critical.css" as="style">
<!-- ✅ 预加载首屏图片 -->
<link rel="preload" href="hero.jpg" as="image">
</head>
<!--===== prefetch:预加载未来页面资源 =====-->
<head>
<!-- ✅ 预加载下一页的资源(低优先级)-->
<link rel="prefetch" href="/page2.html">
<link rel="prefetch" href="/page2.js">
<link rel="prefetch" href="/page2.css">
</head>
<!--
preload vs prefetch:
┌─────────┬──────────┬──────────┬──────────┐
│ 特性 │ preload │ prefetch │ 普通加载 │
├─────────┼──────────┼──────────┼──────────┤
│ 优先级 │ 高 │ 低 │ 中 │
│ 用途 │ 当前页面 │ 未来页面 │ 当前页面 │
│ 时机 │ 立即 │ 空闲时 │ 解析时 │
│ 适合 │ 字体/首屏│ 下一页 │ 常规资源 │
└─────────┴──────────┴──────────┴──────────┘
-->
// ===== 动态 preload =====
function preloadResource(url, type) {
const link = document.createElement('link');
link.rel = 'preload';
link.href = url;
link.as = type;
if (type === 'font') {
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
}
// 预加载字体
preloadResource('/fonts/main.woff2', 'font');
// 预加载下一页脚本
preloadResource('/page2.js', 'script');
策略3:代码分割与懒加载
// ===== Vue 路由懒加载 =====
const routes = [
{
path: '/',
component: () => import('./views/Home.vue') // ✅ 懒加载
},
{
path: '/about',
component: () => import('./views/About.vue') // ✅ 懒加载
}
];
// ===== React 懒加载 =====
import React, { Suspense, lazy } from 'react';
const Home = lazy(() => import('./pages/Home')); // ✅ 懒加载
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
// ===== Webpack 代码分割 =====
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
priority: 5
}
}
}
}
};
// 结果:
// - vendors.js(第三方库)
// - common.js(公共代码)
// - page1.js(页面1)
// - page2.js(页面2)
// ✅ 减少首屏加载体积
六、Chrome DevTools 性能分析
// ===== Performance 面板分析 =====
const PerformanceAnalysis = {
指标: {
FCP: 'First Contentful Paint(首次内容绘制)',
LCP: 'Largest Contentful Paint(最大内容绘制)',
TTI: 'Time to Interactive(可交互时间)',
TBT: 'Total Blocking Time(总阻塞时间)',
CLS: 'Cumulative Layout Shift(累积布局偏移)'
},
优化目标: {
FCP: '< 1.8s',
LCP: '< 2.5s',
TTI: '< 3.8s',
TBT: '< 200ms',
CLS: '< 0.1'
},
查看方式: `
1. 打开 Chrome DevTools → Performance
2. 点击录制按钮
3. 刷新页面
4. 停止录制
5. 分析:
- Main(主线程):查看 JS 执行时间
- Network(网络):查看资源加载时间
- Frames(帧):查看渲染性能
`
};
// ===== Lighthouse 自动分析 =====
// 打开 Chrome DevTools → Lighthouse → Generate report
// 自动给出性能评分和优化建议 ✅
七、总结与记忆口诀 📝
核心记忆
阻塞规则:
- JS:全阻塞(阻塞 DOM 解析、渲染、后续资源加载)
- CSS:半阻塞(阻塞渲染、阻塞 JS 执行,不阻塞 DOM 解析)
优化策略:
- CSS 放
<head>(尽早加载) - JS 放
<body>底部 +defer(不阻塞) - 关键资源
preload(优先加载) - 非关键资源
prefetch(延后加载)
记忆口诀
CSS 头部早加载
JS 底部 defer 好
async 独立不依赖
preload 关键要记牢
八、面试加分项 🌟
前端面试提升点
- ✅ 能清晰讲解 JS 和 CSS 的阻塞行为
- ✅ 理解 async 和 defer 的区别
- ✅ 知道 preload 和 prefetch 的使用场景
- ✅ 能分析 Performance 面板
业务代码提升点
- ✅ 所有脚本使用 defer
- ✅ 关键字体使用 preload
- ✅ 路由懒加载(代码分割)
- ✅ 图片懒加载(IntersectionObserver)
架构能力增强点
- ✅ 实现资源加载优先级系统
- ✅ 配置 Webpack 代码分割策略
- ✅ 实现首屏性能监控
- ✅ 优化 Core Web Vitals 指标
记住:JS 全阻塞,CSS 半阻塞,优化加载是关键! ⚡
#前端面试小册##前端##银行##阿里##快手#前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
查看22道真题和解析
