去掉你代码里的 document.write(script...

2024-06-01 06:19:51

在传统的浏览器中,同步的 script 标签是会阻塞 HTML 解析器的,无论是内联的还是外链的,比如:

<script src="a.js"></script><script src="b.js"></script><script src="c.js"></script><img src="a.jpg">

在这个例子中,HTML 解析器会先解析到第一个 script 标签,然后暂停解析,转而去下载 a.js,下载完后开始执行,执行完后,才会继续解析、下载、执行后面的两个 script 标签,最后解析那个 img 标签,下载图片,展现图片。假设每个文件的下载时间都是 1 秒,且忽略浏览器的执行耗时,那么你最终会在第 4 秒结束时看到 a.jpg 渲染在了浏览器上。

如今的浏览器已经不再这么线性的执行了,在遇到第一个 script 标签后,主线程中的解析器暂停解析,但浏览器会开启一个新的线程去于预解析后面的 HTML 源码,同时预加载遇到的CSS、JS、图片等资源文件,也就是说,在现代浏览器中,上面这个例子中的四个资源文件是会被并行下载的,所以不考虑浏览器的执行耗时的话,渲染出最后那张图片只需要 1 秒钟。

额外小知识:

但浏览器能做的仅仅是预解析和预加载,脚本的执行和 DOM 树的构建仍然必须是线性的,从而页面的渲染也必须是线性的。脚本必须顺序执行这很好理解,比如 b.js 很可能用到 a.js 里的变量;DOM 树不能提前构建的原因也能想到,a.js 里很可能去查询 DOM 树,在那时执行 querySelectorAll("script").length 必须是 1,img 的话必须是 0。

但还有一个东西也能解释上面两个优化不能做的原因,甚至也能让预解析和预加载这两个已经做了的优化失效的东西,那就是 document.write(),document.write 可以在当前执行的 script 标签之后插入任意的 HTML 源码,如果你插入一个 "<div>foo</div>" 那还好,但如果插入一个未闭合的开标签呢,比如:

<script>document.write("<textarea>") // 还可以是 document.write("<!--") 等</script><script src="a.js"></script><script src="b.js"></script><script src="c.js"></script><img src="a.jpg">

当第 1 个 script 标签执行完毕后,浏览器就会发现,因为 document.write 输出了一个未闭合的开标签,所以刚才做的预解析成果得全部扔掉,重新解析一次,第二次解析后 script 标签和 img 标签都成了 textarea 的内容了,因此预加载的 JS 和图片资源都白加载了。但这种情况毕竟是少数,预解析的利远远大于弊,所以浏览器们才做了这个优化,MDN 上有一篇文章 列举了一些会让浏览器做的预解析优化失失效的代码。

本文的主角是用 document.write 输出一个 script 标签的情况,比如:

<script src="a.js"></script><script>document.write('<script src="http://thirdparty.com/b.js"><\/script>')</script><script src="c.js"></script>

这个例子中,由于 b.js 是通过 JS 代码插入的,HTML 预解析器是看不到的,所以只有当 a.js 下载并执行完毕,且第二个内联的 script 执行完毕后,b.js 才会开始下载,也就是说,b.js 不能和 a.js 及 c.js 并行下载了,从而导致页面展现变慢,同样假设每个文件的下载时间都是 1 秒,那么这三个文件下载执行完就需要两秒,就因为 b.js 不能预加载。在一个外链的 JS 文件比如 a.js 中执行 document.write("<script...) 也是类似的效果。

Chrome 的工程师们最近发现,因这种包含于 document.write() 中的 script 标签而导致的页面加载变慢的情况非常普遍,同时还发现了个普遍的规律,那就是这些脚本的 URL 如果不是本站的(跨站的),一般都是些广告和统计功能的第三方脚本,是对页面正常展现非必须的,如果是本站的,则更可能是当前页面展现所必须的脚本。

这些工程师们还在 Chrome for Android 中针对 2G 环境做了采样统计,发现有 7.6% 的页面包含了至少一个这样的 script 标签,而且发现假如禁止加载这些非必要的脚本后,页面本身的展现速度会有显著提升:

用 document.write 去加载脚本,绝大多数情况下都是错误的做法,是应该被优化的。那该怎么优化呢?改成普通的 script 标签放在 HTML 里面吗?不行也不该,先来说说为什么不行,一般来说,一个脚本之所以要放在 JS 里去加载,而不是直接放在 HTML 里,可能的原因有:

1. 脚本的 URL 是不能写死的,比如要动态添加一些参数,用户设备的分辨率啊,当前页面 URL 啊,防止缓存的时间戳啊之类的,这些参数只能先用 JS 获取到,再比如国内常见的 CNZZ 的统计代码:

<script>
var cnzz_protocol = (("https:" == document.location.protocol) ? " https://" : " http://");
document.write(unescape("%3Cspan id='cnzz_stat_icon_30086426'%3E%3C/span%3E%3Cscript src='" + cnzz_protocol + "w.cnzz.com/c.php%3Fid%3D30086426' type='text/javascript'%3E%3C/script%3E"))
</script>

它之所以为用户提供 JS 代码,而不是 HTML 代码,是为了先用 JS 判断出该用 http 还是 https 协议。

2. 在外链的脚本里加载另外一个脚本,这种情况就没法写在页面的 HTML 里面了,比如百度联盟的这个脚本里就可能用 document.write 去加载另外一个脚本:

http://cpro.baidustatic.com/cpro/ui/c.js

再来说说为什么不该,即便真的有少数的代码可以优化成 HTML 代码,比如上面这个 CNZZ 的就可以改成:

<span id='cnzz_stat_icon_30086426'></span><script src='//w.cnzz.com/c.php?id=30086426' type='text/javascript'></script>

这样浏览器就可以预加载了,算是进行优化了,但这并不是最佳的优化,因为,当你能明显感觉到你的页面因为第三方脚本的原因导致展现缓慢,通常都不是因为它没有被预加载,而是因为它的加载速度比你自己网站的脚本加载速度慢太多,再拿出这个例子:

<script src="a.js"></script><script>document.write('<script src="http://thirdparty.com/b.js"><\/script>')</script><script src="c.js"></script>

thirdparty.com 网站出问题的时候,a.js 和 c.js 1 秒就加载完了,而 b.js 也许需要 10 秒才能加载完,那 c.js 的执行以及后面的 HTML 的渲染就需要等 10 秒钟,极端情况就是 b.js 一直卡在那里直到超时,如果这些脚本是放在 head 里的,那用户永远不会看到你的页面,在国内的人应该早已深有体会,就是那些引用了 Google 统计、广告等同步版脚本的页面,这种情况下只靠预加载是解决不了根本问题的。

最佳的做法是把它改成异步执行的,异步的 script 根本不会阻塞 HTML 解析器,也就用不到预解析了。通过 HTML 载入的 script 可以用 async 属性将它变成异步的:

<span id='cnzz_stat_icon_30086426'></span><script async src='//w.cnzz.com/c.php?id=30086426' type='text/javascript'></script>

当然,这个外链的脚本本身也可能需要做相应的调整,比如万一里面还有个 document.write,那整个页面就会被覆盖了。

上面也说到了,大部分第三方脚本都需要添加动态参数,没法修改成 HTML 的代码,所以更加常见的做法是用 document.createElement("script") 配合 appendChild/insertbefore 插入 script,以这种方式插入的 script 都是异步的,比如:

<span id='cnzz_stat_icon_30086426'></span><script>document.head.appendChild(document.createElement('script')).src = '//w.cnzz.com/c.php?id=30086426'</script>

目前国内国外绝大多数的广告、统计服务提供商都有提供异步版本的代码,但也有可能没有,比如 CNZZ 的统计代码, 看 这里 和 这里 。

本着用户体验至上的原则,Chrome 的工程师们准备进行一个大胆的尝试,那就是屏蔽掉这种脚本,具体的屏蔽规则是,符合下面所有这些条件的 script 标签对应的脚本不会再被 Chrome 执行:

1. 是用 document.write 写入的

无法预解析和预加载

2. 同步加载的,也就是不带有 asyc 或 defer 属性的

即便写在 document.write 里,异步的 script 标签也不会阻塞后面脚本的执行以及后面 HTML 源码的解析

3. 外链的

内联的反正没有网络请求,不影响展现速度,况且谁会去写 <script>document.write("<script>alert('foo')<\/script>")</script> 这样的代码。。

4. 跨站的

上面说过了,跨站的脚本影响页面本身的内容展现的可能性更小,跨站和跨域的区别,请看我的这篇文章

5. 所在页面的此次加载不是通过刷新操作触发的

虽然说第三方脚本影响页面主体内容和功能的可能性不大,但仍然有这个可能,假如页面主体内容收到影响了,用户必然会点刷新,所以刷新的时候,这个屏蔽逻辑得关掉

6. 所在页面是顶层的(self === top),而不是 iframe

因为 iframe 往往是广告之类的小区块,而用户想看的主页面通常是这些 iframe 的父页面,且 iframe 内的脚本并不会阻塞父页面的渲染,所以没必要优化它们

7. 未被缓存

如果这个外链脚本已经被缓存了,当然可以直接拿来执行了。

但这毕竟是个 breaking change,考虑用户体验的同时也不能不考虑网站本身,所以这个改动会循序渐进的一步一步(我总结成了 4 步)执行,给开发者留出修改自己代码的时间,具体计划是:

1. 警告

从 Chrome 53,也就是目前的稳定版开始,开发者工具的控制台中会出现下面这样的警告(即便脚本已经被缓存或者页面是通过刷新操作打开的,也会出现这个警告):

qMJNVrf.png!web

2. 在 2G 网络下开启屏蔽( issue 640844 )

从 Chrome 54(2016 年 10 月中旬发布)开始,在 2G 网络环境下开启屏蔽。需要指出的是,屏蔽一个脚本并不是真的不发起请求,而是会发一个异步的请求,且优先级很低(优先级为 0,Chrome 给每个 http 请求都标有优先级)。这个异步请求的目的不是为了去执行它(上面也说了,把一个同步脚本直接当成异步脚本去执行,是很可能会出问题的),而是为了:

(1)为了把脚本放到缓存里,也就是说,第一次屏蔽了,第二次翻页等操作后如果还需用到那个脚本,那它很可能已经在缓存里了,这也是为了减少 breaking 的概率。

(2)为了通知这个脚本所在的服务器,“你的脚本被我屏蔽了”。脚本被屏蔽后异步发起的请求会被 Chrome 添加一个特殊的请求头 Intervention,值是一个对应的 chromestatus 网址:

Intervention: <https://www.chromestatus.com/feature/5718547946799104>

如果你是一个第三方服务提供者,比如广告投放系统的负责人,你在你的服务器的访问日志里看到这个请求头,就说明你的脚本已经被屏蔽了,从 Referer 头里也能看到被屏蔽的脚本是在哪个页面里被引用的,然后你需要做的是就是让这个网站把你们提供的代码更新成异步版本的。

因为是 2G,所以肯定是移动版的 Chrome,也就是 Chrome for Android,Android WebView 不知道不会开启,在 6 月份 Chrome 官方 发布的消息 中说到还没有定要不要在 WebView 中开启:

Will this feature be supported on all six Blink platforms (Windows, Mac, Linux, Chrome OS, Android, and Android WebView)?

This feature will be enabled on Win, Mac, Linux, ChromeOS and Android, but we are still deciding whether it's appropriate to apply this intervention for WebView.

Chrome for IOS 内核不是 blink,不受影响。

为了方便调试,在 Chrome PC 版开发者工具中将网络切换成 2G 也能触发这个屏蔽规则( 还在实现中 )。

我自己看到的一个到时候可能受到影响的手机网站: https://sina.cn/

3. 在网速较差的 3G 和 WiFi 环境下开启屏蔽( issue 640846 )

目前还没有决定从哪个版本开始,如果上一个 2G 阶段进行顺利,才可能会进入这个阶段,等有消息的时候我会在这里追加具体开启的版本号,PC 页面在这个阶段才会受到影响。

我自己看到的两个到时候可能受到影响的网站: https://www.baidu.com/ https://www.taobao.com/

4. 完全屏蔽

任何网络环境都开启屏蔽,这完全是我的猜测,还没有看到 Chrome 的人在讨论,但即便最后要这样做了,肯定也需要较长的过度时间。

有些同学可能会问:“我把它放在页面最底部,总该没事了吧”。别忘了同步的 script 会阻塞 DOMContentLoaded/load 事件,关掉 *** 运行下面的 demo 试试:

<script>document.addEventListener("DOMContentLoaded", function(){alert("执行异步渲染、绑定事件等操作")
})
document.write("<script src=http://www.twitter.com><\/script>")</script>

用 jQuery 的话,所有 $(function(){}) 里的回调函数都会被卡主,问题依然很严重。

最后总结一下:“为什么说 document.write("<script...) 不好” - “因为它本来能够写成异步的,却写成了同步且不能预加载的”

PS:Chrome 还在做另外一个 优化的尝试 ,就是开启一个单独的 V8 线程用来执行那些包含有 document.write("<script...) 字样的内联的 script 标签中的代码从而预加载那个脚本,但就像我上面说的(预加载不能解决阻塞问题),即便这个优化真做成了,意义也不大。

PPS:HTML 规范也做了 对应的修改 ,说允许浏览器做这种优化。

转载于:https://blog.51cto.com/jinjiang2009/1853794

去掉你代码里的 document.write(script...相关推荐

  1. Jsoup代码解读之三-Document的输出

    转载自   Jsoup代码解读之三-Document的输出 Jsoup官方说明里,一个重要的功能就是***output tidy HTML***.这里我们看看Jsoup是如何输出HTML的. HTML ...

  2. javascript里的document.all用法收集

    javascript里的document.all用法   从IE4开始IE的object model才增加了document.all[],来看看document.all[]的Description: ...

  3. SAP QM 如何在SAP系统里审批挂在Quality Notification里的document?

    SAP QM 如何在SAP系统里审批挂在Quality Notification里的document? 如下的Quality Notification单据里有附上一个WORD文档, 单据号是10000 ...

  4. 如何缺心眼的在代码里下毒

    偶然看到一篇脑洞大开的文章,转载过来乐呵一下,原文地址:https://www.jianshu.com/p/635fcf4fe594 下毒要点 独特的算法,个性的变量命名. 复杂的结构,畸形的文件路径 ...

  5. 如何在代码里配置-D 参数?

    搜索了,没有找到满意答案,请教一下,如何在代码里配置-D 参数? 比如下面的代码,怎么配置"-Djava.security.policy="? 谢谢!! if (System.ge ...

  6. SAP PM 初级系列23 - IW22 事务代码里创建维修工单

    SAP PM 初级系列23 - IW22 事务代码里创建维修工单 SAP PM模块里,事务代码IW22用于修改一个已经存在的维修通知单. 实际上在这个界面里,不仅可以修改维修通知单相关的数据,而且可以 ...

  7. SAP MM MI01事务代码里的批次确定

    SAP MM MI01事务代码里的批次确定 1 – 批次管理启用之后果 一个物料如果启用了批次管理,那么库存管理以及盘点等诸多事务里都需要在批次的层次上进行. 货物移动的时候,需要在界面上指定相关货物 ...

  8. JAVA 代码里中文乱码问题

    为什么80%的码农都做不了架构师?>>>    1.中文在代码里, 输出到控制台出现了乱码 解决方法:右键项目属性,修改编码格式为UTF-8,重新打包,部署启动.即可. 转载于:ht ...

  9. 不要再代码里频繁的new和delete

    为什么不要再代码里频繁的new和delete了呢,因为new是在堆中搜索一块可用的内存给程序使用,在堆中分配的内存不是连续的,不像栈,后进先出,你不可能在栈的中间pop出一块内存,所以想要使用栈中某一 ...

  10. 牛客网_PAT乙级_1023旧键盘打字(20)【别人代码里用到的hash是啥】

    心得 关于如何找到个别测试点通不过的原因: 复制别人的正确的代码,和自己的代码运行相同的测试用例,比较两者之间的区别 ??别人代码里用到的hash是啥?? 题目描述 旧键盘上坏了几个键,于是在敲一段文 ...

最新文章

  1. mysql分库一个库和多个库_数据库分库后不同库之间的关联
  2. Nginx会话保持之nginx-sticky-module模块
  3. 【Java 并发编程】线程池机制 ( 线程池阻塞队列 | 线程池拒绝策略 | 使用 ThreadPoolExecutor 自定义线程池参数 )
  4. 干净卸载mysql (注册表)
  5. 国际计算机杂志排名2015,中国计算机学会推荐国际学术刊物与期刊(新增列表)2015-12-22-06_48_31...
  6. 谷歌自锤Attention:纯注意力并没那么有用,Transformer组件很重要
  7. boost::statechart模块实现无效结果复制测试
  8. 【做题记录】[NOIP2016 普及组] 魔法阵
  9. e盾网络验证源码_Laravel [mews/captcha] 图片验证码
  10. LeetCode刷题(2)
  11. OSGI开发web应用
  12. python爬取喜马拉雅vip音频安卓_Python爬虫:爬取喜马拉雅音频数据详解
  13. Oracle OCP题库变了,052全新题库收集整理-30
  14. c51语言转换ASCII码,ASCII 码和十六进制数的转换 -51单片机
  15. Excel 精选28个技巧
  16. 从计算机视觉到人脸识别:一文看懂颜色模型、信号与噪声
  17. CIS基线合集-常用版本操作系统、数据库及中间件
  18. 飞机大战,坦克大战源码、简单仿记事本、错题本源码及笔记
  19. 数据库原理与应用实验十 数据库完整性实验
  20. POE交换机和普通交换机哪里不同?POE交换机和普通交换机哪个好?

热门文章

  1. CDN加速的四大解决方案
  2. 征服 Apache + SSL
  3. QQ号被盗了申诉回来马上又被盗了怎么办
  4. uni.navigateTo页面跳转时传对象参数
  5. 停课集训 11.27
  6. 【MySQL】014-join连接语句用法详解
  7. 通过requests获取网络上图片的大小
  8. [附源码]Python计算机毕业设计常见病辅助食疗系统
  9. php 足迹 表设计,成长的足迹设计方案
  10. 中等计算机的配置,中等特效的电脑主机配置推荐