Skip to content

JS 的渲染与运行 ​

元信息 ​

  • 目标:掌握浏览器处理 JS 的方式——解析为 AST、编译为字节码、在渲染主线程上执行;从此能从原理层面解释脚本为何打断 HTML 解析、defer/async 这些"惯例"的由来
  • 关键概念:渲染主线程、解析阻塞、执行阻塞、async/defer、CSS 对脚本的间接阻塞
  • 关联阶段:cp3
  • 常见误区:以为脚本仍应放页面底部(defer 出现后放 head 既早下载又不阻塞);以为 async/defer 对内联脚本也有效(仅对外联脚本有效);以为 CSS 会阻塞 HTML 解析(CSS 只阻塞渲染成像,但会间接阻塞其后的脚本)

在关键渲染路径的流水线里,CSS 只需被解析成 CSSOM,JS 却要在渲染主线程上运行——它能改 DOM、查样式,浏览器对它也就最严格:默认停下 HTML 解析等它。本篇讲 JS 如何被处理与执行、阻塞从何而来、defer/async 如何化解。

前置 ​

  • 关键渲染路径:渲染流水线与 CSS 的渲染阻塞——本篇从 JS 一侧接着讲

从源码到执行 ​

  • JS 被解析为抽象语法树(AST),现代引擎(如 V8)进一步编译为字节码,在渲染主线程上解释/执行
  • 主线程只有一条:DOM 不能被两个线程同时修改,所以布局、绘制与 JS 执行共享同一条主线——脚本跑多久,页面就卡多久(web worker 等机制可另开线程跑 JS,但不能碰 DOM)
  • 跑完之后还有账要算:脚本是运行时改样式的头号来源——改几何属性(宽高、位置)触发"布局→绘制→合成"整段重跑,只改颜色等则可跳过布局。局部重跑的机制见关键渲染路径,代价分层第 8 周性能话题还会回来

解析阻塞与执行阻塞 ​

<script src="app.js"></script>(无属性)是解析阻塞资源:解析器停下,等脚本下载并执行完毕,才继续解析后续 HTML。为什么这么严格?因为脚本可能 document.write 改写后面的文档,也可能查询/修改已有元素,顺序错了结果就错。

连带效应:脚本执行前还必须等 CSSOM 就绪(脚本经常查询元素的计算样式),所以CSS 会间接阻塞其后的脚本——渲染阻塞(见关键渲染路径)不只推迟成像,也拦住了脚本。

网络等待与加载模式 ​

外联脚本都要走网络。解析器等待网络返回是"阻塞式";不等待、先继续解析则是"非阻塞式"——但非阻塞让就绪顺序变得不可预测,需要 async/defer 这类机制来声明依赖关系。

要点:

  • async 与 defer 仅对外联脚本有效,内联脚本无此属性
  • defer 的存在让"脚本放 <head>"重新成为最佳实践——既早下载,又不阻塞、不怕元素未就绪。"把脚本塞到页面底部"是 defer 出现之前的时代应对,不必再效仿
<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

三种写法在下载、执行时机、顺序上的逐项对比,见异步与阻塞。

让脚本不挡路,正是优化关键渲染路径的第一条原则(减少关键资源数量)在脚本一侧的落地——三条原则见关键渲染路径。

延伸 ​

  • 网络不可靠的实况、三种加载模式的执行时序、动态加载脚本、同步/异步×阻塞/非阻塞的概念辨析,见异步与阻塞