Html中Css和script的阻塞情况分析

背景

今天又是碰到诡异bug的一天。我们运营会用到 Google Analytics 埋点。埋点sdk的安装脚本大体都是这样的:

html 复制代码
  <script>
  !(function(w,d,s,l,i) {
    w[l]=w[l]||[];
    var f=d.getElementsByTagName(s)[0],
        j=d.createElement(s);
        j.async=true;
        j.src=i;
        f.parentNode.insertBefore(j,f);
  })(window, document, 'script', 'dataLayer', 'https://www.google-analytics.com/analytics.js');    
  </script>

动态创建一个带 async 的 script 标签请求埋点脚本。正常来说,加了 async 的脚本下载是不会阻塞页面渲染的。

然而今天在 ios webview (wkwebview) 中,用国内网络访问这个国内不存在的资源时,长时间的loading阻塞了首屏渲染。而用移动端的 Safari 打开又不会有阻塞问题。并且也不是每个页面都出现这个情况。

一开始怀疑是业务代码的问题,于是一行行排查,但最后发现,删除页面css中的某一行时,页面就不阻塞了。并且每个有问题的页面,需要删除的css都不一致,没什么特殊性。

那完犊子了,一行css的增删怎么会阻塞页面渲染,而且只在ios webview中出现,十有八九是webview的bug了。

一番搜索,只找到个类似的问题 脚本下载会阻塞 Mobile Safari 首屏渲染,虽然不一样,但解决思路是可以借鉴的。就是通过> requestAnimationFrame 和 microTask 延迟了 script 脚本的下载,使其在首屏渲染之后再开始下载;

问题解决了,但引发了我对css和js加载导致页面阻塞这个经典问题的思考,之前只是死记一直没有实践过,本着没有困难创造困难也要上的精神,特此水文一篇!

前置知识

  • 当页面因为加载外部资源而阻塞时,页面可能会优先渲染已经解析好的内容

以 chrome 为例,以下是一张经典老图,我们知道从文档开始解析到页面展示,主要经历了 html的解析,dom树的构建,css的解析,cssom的构建,再通过二者创建render tree,重排,重绘,渲染;而我们常遇到的阻塞问题,主要发生在 html解析 和 页面渲染。

需要注意的是,这是一个渐进的过程。为了更好的用户体验,渲染引擎会尽可能快的渲染内容,而不是等待所有的 html解析完成。也就是说,当页面因为加载外部资源而阻塞时,页面可能会优先渲染已经解析好的内容。

  • 在解析文档时,浏览器会有额外的线程找出需要下载的外部资源(js/css/img),并开始并行下载,也就是说html解析阻塞并不影响资源下载;

环境准备

起一个 parsing-demo 文件夹,内容如下

  • index.html
html 复制代码
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
</head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <div id="down">下</div> 
</body>
</html>
  • dist/test.js
js 复制代码
console.log('-------- test 执行啦 ---------')
  • dist/sleep.js
js 复制代码
function sleep(delay) {
  var start = new Date().getTime();
  while (new Date().getTime() - start < delay) {
    continue;
  }
  console.log('sleep done');
}
sleep(5000);
  • dist/test.jpg
  • dist/test.css
css 复制代码
#up {
  background-color: pink;
}
#down {
  background-color: skyblue;
}

因为懒得再起个服务器,我这里主要用 Charles 打断点来模拟请求阻塞

我这里是macos,先用 helm 配置一下 www.test.com 的 DNS 解析,解析到本地

127.0.0.1 www.test.com

然后执行

shell 复制代码
npx http-server dist -p 80

通过 http-server 起一个静态服务器,指定dist为根目录,通过参数 -p 设置端口为 80,这样就可以模拟访问外部资源啦

再开个终端执行

shell 复制代码
npx http-server

不指定额外参数,这样 http://127.0.0.1:8080/ 默认访问的就是我们的 index.html 了,测试下

页面可以正常访问;

最后再配置一个 Charles 的断点,用来拦截所有对 www.test.com 请求的响应,模拟下载阻塞(不会Charles的赶紧学啦!)

外部Css的阻塞情况分析

我们在head里加上css

html 复制代码
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
  <link rel="stylesheet" href="http://www.test.com/test.css">
</head>

访问 http://127.0.0.1:8080/

可以看到css的响应被正常拦截,页面一片空白,而 elements 已经能看到 html 的全部内容了;

放开 test.css 的断点,可以看到页面正常渲染

这样就验证了,外部css的下载不会阻塞 html 的解析,但会阻塞页面的渲染

这个很好理解,因为 css 并不能修改 dom,所以不应该阻塞 html 的解析,而 css 是可以修改页面样式的,所以会阻塞页面渲染;

大部分没有钻研精神的人都到此为止了。但!这只是把 <link> 放在 <head> 的情况,那么如果把 <link> 放在 <body> 里,结论还一样吗?(其实是我一开始手欠放错位置了...坑了我好久😭)

我们把 <link> 放到 <body> 里再试试看

可以看到页面在 link 加载时,阻塞了剩余 html 的解析,并且优先渲染了已解析的 html;这让我一度怀疑网上的文章是不是都是乱写的。我又拿 safari 和 firefox 测试了以下,发现三家表现都不一样;

safari 不阻塞解析,阻塞渲染

firefox 既不阻塞html解析 ,也不阻塞渲染

这时验证的结果已经和我们平时看到的文章有些出入了

script 的阻塞情况分析

我们知道 js 是可以修改 dom 的,为了防止html的解析和预期不一致,因此的 js 的下载和执行都会阻塞 html 解析,我们可以用以下例子验证。断点解开前,html解析阻塞;先解开 test.js 的断点,再解开 sleep.js 的断点,在sleep执行期间,html解析也会阻塞;

html 复制代码
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>前端一块砖</title>
  <script src="http://www.test.com/sleep.js"></script>
</head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <script src="http://www.test.com/test.js"></script>
  <div id="down">下</div> 
</body>
</html>

那么会不会阻塞渲染呢?

为了让页面有东西可渲染,script 就要放到 body 里了,发现三家表现又不一致;

chrome 和 safari 阻塞了 html 解析,优先渲染了已解析的内容

而 firefox 既阻塞了解析又阻塞了已解析内容的渲染

验证到这里,事情已经开始有点不对劲了🤨

我们再验证一下 当文档 link的css后面有 script 时,script 需要等待前面的css下载执行完毕才会执行,也就是说css的下载会阻塞后方js的执行,导致html解析的阻塞,我们可以用下面这段例子验证

html 复制代码
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>前端一块砖</title>
    <link rel="stylesheet" href="http://www.test.com/test.css">
  </head>
<body>
  <div id="up">上</div> 
  <img width="50" height="50" src="http://www.test.com/test.jpg" alt="">
  <script src="http://www.test.com/test.js"></script>
  <div id="down">下</div> 
</body>
</html>

可以看到 test.js 下载时阻塞了 html 的解析,我们先断点放行 js 的下载,继续阻塞 css,在 chrome 中页面依然没渲染,下载好的js也没有执行;

这是因为 css 可能会改变页面样式,而导致后面 js 获取的页面样式可能和预期不一致(因为 css 可能比js先下载好,也可能比js晚下载好),为了避免不符合预期的情况,js 的执行需要等待 css 下载执行完成(内联的 script 也是一样);

以上都是对于静态 script 标签的情况,而对于动态创建的 script 标签:无论是否带 async 标记的,标签创建完成都会立即开始下载,下载过程中都不会阻塞html解析和渲染,并且下载完成后立即执行

以上就是 css 和 script 在 html 解析过程中的大部分情况了;

回到上面各浏览器表现不一致的问题,其实就是各浏览器的实现不一致,各浏览器都有自家的渲染时机。可以自行验证一下,link 和 script 无论是放在 <head> 还是 <body> 中,各家表现其实都不一致。所以对于 css 和 script 导致的 html 解析阻塞和渲染阻塞并没有一个标准的答案。我觉得也没有必要去硬记各家的表现,具体情况具体分析就好了,如果说非要一个面试的答案的话,以下是基于 Chrome 的总结

总结

基于 chrome 119.0.6012.0(正式版本)canary (arm64)

  1. 阻塞通常发生在 html 解析 和 页面渲染阶段
  2. 文档解析时,浏览器会有额外线程找出需要下载的外部资源,提前开始下载,html解析阻塞不会影响外部资源下载
  3. css,当 link css 放在 head 时,css 的下载不会阻塞 html 的解析,但会阻塞页面的渲染,当 link 放在 body 时,阻塞 html 解析,不阻塞已解析部分的渲染
  4. script,静态script标签的下载和执行阻塞html的解析,但会先渲染已解析部分;script 之前有 link css 时,会等待css下载执行完成再执行
  5. 动态创建的script标签不会阻塞html解析,创建完成立即开始下载,下载完成立即执行,执行时阻塞html解析

参考

脚本下载会阻塞 Mobile Safari 首屏渲染

How browsers work

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书