【前端面试小册】浏览器-第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 解析)

优化策略

  1. CSS 放 <head>(尽早加载)
  2. JS 放 <body> 底部 + defer(不阻塞)
  3. 关键资源 preload(优先加载)
  4. 非关键资源 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天学完,上岸银行总行!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务