JavaScript 文件在页面渲染中的加载机制详解

178 阅读2分钟

JavaScript 文件在页面渲染中的加载机制详解

1. 基本加载流程

当浏览器解析 HTML 遇到 <script> 标签时,会按照以下顺序处理:

graph TD
A[HTML 解析] --> B[遇到 script 标签]
B --> C{是否 defer/async?}
C -->|否| D[停止 HTML 解析]
D --> E[下载 JS 文件]
E --> F[执行 JS 代码]
F --> G[恢复 HTML 解析]
C -->|defer| H[异步下载,延迟执行]
C -->|async| I[异步下载,立即执行]

2. 不同 script 加载模式的对比

加载方式HTML 解析是否阻塞JS 执行时机执行顺序保证
普通 script阻塞下载完成后立即执行按文档顺序
async script不阻塞下载完成后立即执行不确定
defer script不阻塞DOMContentLoaded 前顺序执行按文档顺序
module script默认 defer类似 defer按文档顺序

3. 关键阶段详解

(1) 普通 script (无属性)

<script src="app.js"></script>
  • 阻塞行为
    • 停止 HTML 解析
    • 同步下载并执行 JS
    • 执行完后才继续解析 HTML
  • 典型影响
    • 若 JS 文件过大,会导致首屏渲染延迟(白屏时间长)

(2) async 脚本

<script async src="analytics.js"></script>
  • 特点
    • 异步下载(不阻塞 HTML 解析)
    • 下载完成后立即执行(可能中断 HTML 解析)
    • 多个 async 脚本的执行顺序无法保证
  • 适用场景
    • 不依赖 DOM 的独立脚本(如统计分析)

(3) defer 脚本

<script defer src="vendor.js"></script>
  • 特点
    • 异步下载(不阻塞 HTML 解析)
    • 在所有 HTML 解析完成后,按文档顺序执行
    • DOMContentLoaded 事件前触发
  • 适用场景
    • 需要操作 DOM 但又不想阻塞渲染的脚本

4. 现代浏览器优化机制

(1) Preload 预加载

<link rel="preload" href="critical.js" as="script">
  • 提前下载但不执行
  • 适用于关键资源加速

(2) Prefetch 预获取

<link rel="prefetch" href="next-page.js">
  • 空闲时下载后续页面资源
  • 优先级低于 preload

5. 性能优化建议

  1. 关键 JS 内联或 preload

    <!-- 内联关键代码 -->
    <script>/* 关键渲染路径代码 */</script>
    
    <!-- 非关键代码异步加载 -->
    <script defer src="non-critical.js"></script>
    
  2. 第三方脚本异步化

    <script async src="https://analytics.example.com/script.js"></script>
    
  3. 模块化拆分

    // 动态导入非首屏需要的代码
    button.addEventListener('click', () => {
      import('./modal.js').then(module => module.open());
    });
    
  4. 使用 type="module"

    <script type="module" src="app.js"></script>
    
    • 现代浏览器会自动应用 defer 行为
    • 支持 ES6 模块语法

6. 面试回答示例

问题:"JS 文件加载会如何影响页面渲染?有哪些优化手段?"

回答: "浏览器遇到普通 script 标签时会阻塞 HTML 解析,直到 JS 下载并执行完成。这会导致渲染延迟,对此我们有几种优化方案:

  1. 对非关键脚本使用 async/defer 避免阻塞:

    • async 用于独立脚本(如数据分析)
    • defer 用于需要 DOM 但可延迟的脚本
  2. 通过 preload 提前加载关键资源:

    <link rel="preload" href="critical.js" as="script">
    
  3. 代码拆分和动态导入:

    // 按需加载非首屏代码
    import('./heavy-module.js').then(...)
    

在实际开发中,可以通过将第三方脚本异步化 + 关键脚本 preload,使 LCP 时间减少了 "


可视化时间线对比

普通 script:
[HTML解析=======停止===>][JS下载|执行][继续解析HTML]

async:
[HTML解析======================]
       [JS下载|执行](随机时机)

defer:
[HTML解析======================][DOMContentLoaded][按序执行defer脚本]

ps:理解这些机制可以帮助我们更精确地控制页面加载性能。