浏览器资源加载 - childlabor/blog GitHub Wiki

1.为什么 Javascript 要是单线程的?

如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源, 假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。 当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行

2.为什么 JS 阻塞页面加载 ?

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。 因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。 当 JavaScript 引擎执行时 GUI 线程会被挂起。

  1. css 加载会造成阻塞吗 ?

DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的==解析==。

由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染因此,CSS 加载会阻塞 Dom 的==渲染==。

由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此 GUI 渲染线程与 JavaScript 引擎为互斥的关系。样式表需要在后面的js执行前先加载执行完毕,所以 css 会阻塞其==后面js的执行==。

浏览器主要组件

webkit渲染引擎流程

DOMContentLoaded 与 load 的区别

DOMContentLoaded 事件触发时,仅当 ==DOM解析完成==后,不包括样式表、图片。我们前面提到 CSS 加载会阻塞 Dom 的渲染和后面 js 的执行,js 会阻塞 Dom 解析。

所以我们可以得到结论:当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。

将脚本放到 body 内,只是能一定程度上解决dom被阻塞(提前解析部分html),但是还是需要等待脚本加载并执行完毕后才会触发 DOMContentLoaded 事件。

真正能提升 DOMContentLoaded 的是:script标签的 async 和 defer 属性(下载script并不阻断HTML解析)。

async标记的Script异步执行下载,并执行。这意味着script下载时并不阻塞HTML的解析,并且下载结束script马上执行。在确保脚本不对dom做任何操作的文件下使用。

跟 async不同, defer scripts在整个文档里的script都被下载完才顺序执行。

script属性影响对比

onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片等==资源已经加载完毕==。

优先级

为了达到重要的资源先加载,浏览器有试探法,尝试对资源进行权重分配,例如CSS会在脚本和图片之前先加载。 因为浏览器在试探权重分配,所以并不总是分配的很正确,通常因为没有足够的信息,浏览器可能做出错误的决定。

浏览器基于自身对资源重要性的判断,为不同类型的资源分配相应的优先级。 例如,页面

中的 <script> 标签将以 High 优先级(比优先级为 Highest 的 CSS 低)在 Chrome 中加载;但是,如果该标签具有异步属性(也就是说它能以异步方式加载和运行),其优先级将更改为 Low。

首屏渲染中的图像优先级高于从屏幕外开始的图像。

在早期浏览器,script资源是阻塞加载的,当页面遇到一个script,那么要等这个script下载和执行完了,才会继续解析剩下的DOM结构,也就是说script是串行加载的,并且会堵塞页面其它资源的加载,这样会导致页面整体的加载速度很慢,所以早在2008年的时候浏览器出了一个推测加载(speculative preload)策略(预解析),即遇到script的时候,DOM会停止构建,但是会继续去搜索页面需要加载的资源(多个js可以并行下载),如看下后续的html有没有img/script标签,先进行预加载,而不用等到构建DOM的时候才去加载。这样大大提高了页面整体的加载速度。

预解析(speculative parsing)

预解析算法提升了浏览器加载页面的性能,并且,无需开发者做什么特殊的工作就可以享用到这个算法带来的性能提升。预解析内容的同时并行下载数量是有限制的。当执行预加载算法的时候,浏览器将不会执行页面上的行内js。通过预解析js对文件下载的阻断已经解决了,但是js阻断dom构建还仍然是个问题。defer和async属性可以设置script标签进行异步的加载,而不影响页面dom的构建。

预加载

async和defer可以处理掉那些页面初始化过程中不重要的scripts。对页面初始化过程最重要的资源,通过使用,浏览器会在第一时间加载这些资源。

<link rel="preload" href="font.woff" as="font" crossorigin>

使用 提取的资源如果 3 秒内未被当前页面使用,将在 Chrome 开发者工具的控制台中触发警告,请务必留意这些警告。

预提取

Prefetch 告诉浏览器这个资源将来可能需要,但是什么时间加载这个资源是由浏览器来决定的。 prefetch预加载的资源优先级是最低的。浏览器空闲(Idle)的时候就会去加载。

<link href="static/js/chunk-235dc5d7.733e974e.js" rel="prefetch">

并发请求

浏览器的并发请求数目限制是针对==同一域名==的。 意即,同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。

// Never exceed 6 delayable requests for a given host.
// 限制的是delayable的请求?

源码看浏览器机制

请求可分为delayable和non-delayable

优先级在Medium以下的为delayable,即可推迟的,而大于等于Medium的为non-delayable的。

// The priority level below which resources are considered to be delayable.
static const net::RequestPriority
    kDelayablePriorityThreshold = net::MEDIUM;

还有一种是layout-blocking的请求

这是当还没有渲染body标签,并且优先级在Medium之上的如CSS的请求。

// The priority level above which resources are considered layout-blocking if
// the html_body has not started.
static const net::RequestPriority
    kLayoutBlockingPriorityThreshold = net::MEDIUM;

ShouldStartRequest函数是规划资源加载顺序最主要的函数,从源码注释可以知道它大概的过程:

// ShouldStartRequest is the main scheduling algorithm.
  //
  // Requests are evaluated on five attributes:
  //
  // 1. Non-delayable requests:
  //   * Synchronous requests.
  //   * Non-HTTP[S] requests.
  //
  // 2. Requests to request-priority-capable origin servers.
  //
  // 3. High-priority requests:
  //   * Higher priority requests (> net::LOW).
  //
  // 4. Layout-blocking requests:
  //   * High-priority requests (> net::MEDIUM) initiated before the renderer has
  //     a <body>.
  //
  // 5. Low priority requests
  //
  //  The following rules are followed:
  //
  //  All types of requests:
  //   * Non-delayable, High-priority and request-priority capable requests are
  //     issued immediately.
  //   * Low priority requests are delayable.
  //   * While kInFlightNonDelayableRequestCountPerClientThreshold(=1)
  //     layout-blocking requests are loading or the body tag has not yet been
  //     parsed, limit the number of delayable requests that may be in flight
  //     to kMaxNumDelayableWhileLayoutBlockingPerClient(=1).
  //   * If no high priority or layout-blocking requests are in flight, start
  //     loading delayable requests.
  //   * Never exceed 10 delayable requests in flight per client.
  //   * Never exceed 6 delayable requests for a given host.

htmltag

waterfall

结合上面的注释和截图可以得到以下信息:

1.高优先级的资源(>=Medium)、同步请求和非http(s)的请求能够立刻加载

2.chunk-... 打包的是一些公共资源,它们都会被默认打包到每个异步加载页面的 chunk 中,因为在每个页面不是必须的,就放在 DOMContentLoaded 后加载了,即使优先级hightest(上图区域2)。

3.只有当layout blocking和high priority(> net::LOW)的资源加载完了,才能开始加载delayable的资源,这个就解释了为什么要等区域2加载完了才能加载其它的图片等资源。

4.区域3中在首屏的图片资源优先级提升。同时加载的资源==同一个域==只能有6个。图片132的请求是外域链接。api请求的域名也与资源的不同,因此不在限制内,单独计算。

实际应用

以vue-cli3构建项目在chrome上运行为例

  1. 减少文件数量。

打包生成的 runtime.js非常的小,gzip 之后一般只有几 kb,但这个文件又经常会改变,我们每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中(index.html 本来每次打包都会变)。

// vue.config.js
 config
          .plugin('ScriptExtHtmlWebpackPlugin')
          .after('html')
          .use('script-ext-html-webpack-plugin', [{
            inline: /runtime\..*\.js$/
          }])
          .end();
  1. 为单前页确认需要先加载的资源加上preload。

需要注意的是:如果使用默认的配置,runtime.js文件也会被加预载,由于上面已经将该文件内联入了html中,因此,会增加了一条肯定失败的请求,造成阻塞。

// console
Failed to load resource: the server responded with a status of 404 (Not Found)

我们可以通过配置preload黑名单来剔除不需要的文件。

// 已将runtime.js内联入html 因此不需要proload
config.plugin('preload').tap(options => {
  options[0].fileBlacklist = options[0].fileBlacklist || [];
  options[0].fileBlacklist.push(/runtime\..*\.js$/);
  return options;
});
  1. 增加适当数量的prefetch。

保证在DOMContentLoaded触发之前不浪费浏览器请求数(保证有6条资源请求),同时,不使过多的prefetch加入,导致无法在DOMContentLoaded触发的第一时间去加载单前页面需要的其他资源。

  1. 静态资源放置于一个或者多个独立域名(子域名)之下,突破浏览器的域名并发限制。

参考

⚠️ **GitHub.com Fallback** ⚠️