We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
网页中加载JS文件是一个老问题了,已经被讨论了一遍又一遍,这里不会再赘述各种经典的解决方案。JS文件可以通过来源来分为两个纬度:第一方JS和第三方JS。第一方JS是网页开发者自己使用的JS代码(内容开发者可控)。而第三方JS则是其他服务提供商提供的(内容开发者不可控),他们将自己的服务包装成JS SDK供网页开发者使用。这篇文章关注的第三方JS文件的加载。
从网站开发者的角度来看,第三方JS相比第一方JS有如下几个不同之处:
如果你的网站上面有很多第三方JS代码,那么“下载速度的不可控”很有可能导致你的网站会被拖慢。因为JS在执行的时候会影响到页面的DOM和样式等情况。浏览器在解析渲染HTML的时候,如果解析到需要下载文件的script标签,那么会停止解析接下来的HTML,然后下载外链JS文件并执行。等JS执行完毕之后才会继续解析剩下的HTML。这就是所谓的『HTML解析被阻止』。浏览器解析渲染页面的抽象流程图如下:
script
第三方JS代码并不受网站开发者的控制,很有可能会出现加载时间长甚至加载失败的情况。这时候就会导致整个页面的加载速度变慢。第三方JS代码越多这种风险越大。按照互联网守则:
网站加载速度越慢,用户流失越多
所以要考虑下如何在有很多第三方JS的情况下,保证他们不影响到网站自己的加载速度。我们可以异步加载这些第三方JS代码。
异步加载JS的方法很多,最常见的就是动态创建一个script标签,然后设置其src和async属性,再插入到页面中。这里有个DEMO。实际操作的代码如下:
src
async
<script> function loadScript(url) { var scrs = document.getElementsByTagName('script'); var last = scrs[scrs.length - 1]; var scr = document.createElement('script'); scr.src = url; scr.async = true; last.parentNode.insertBefore(scr, last); } loadScript('test.js'); </script>
PS:为了避免IE8以前版本的bug,并且确保script能插入DOM树,所以这里没有直接document.body.append(src),而是调用了insertBefore方法。
document.body.append(src)
insertBefore
改成异步加载第三方JS代码之后,在JS的下载过程中浏览器会继续解析渲染HTML。流程图就变成了如下:
因为loadScript的操作也是使用JS实现的,所以在JS下载之前会有一段执行JS代码的消耗。但是这段JS代码很简单,很快就会执行完毕。
loadScript
除了动态创建script标签的方法,异步加载JS的方法还有很多其他奇技淫巧,这里也罗列了一下:
XMLHttpReqeust
JSONP
eval()
iframe
new Image().src = 'http://url.com/sample.js'
Object
动态创建script标签的方法可以异步加载第三方JS,但它也有缺陷。如果加载代码之前有外链JS文件或CSS文件需要下载,如下面的代码:
<script src="app1.js"></script> <script src="app2.js"></script> <script> function loadScript(url) { var scrs = document.getElementsByTagName('script'); var last = scrs[scrs.length - 1]; var scr = document.createElement('script'); scr.src = url; scr.async = true; last.parentNode.insertBefore(scr, last); } loadScript('test.js'); </script>
那么会先下载解析app1.js和app2.js再执行我们的loadScript方法,所以第三方脚本的下载也会被暂停。流程图如下:
app1.js
app2.js
而如今我们页面中代码如此复杂,触发这种case的情况很多。上面的DEMO中实际下载过程也确实是这样,动态创建script标签方式下载的test.js需要等到其他CSS和JS文件下载执行完毕之后才开始下载。如下图:
虽然这对页面原有JS的执行不会有大的影响,但会影响到第三方JS代码本身的下载与执行。如何解决这个问题呢?
你可能已经发现上面的例子有个问题:HTML代码中g.js的位置在test.js之后却先下载了。其实这得益于浏览器的预解析机制,会先对HTML代码做静态分析找到外链的JS和CSS文件,然后并行下载下来(但是执行顺序不变)。IE>=8 及其他主流浏览器基本都实现了这个功能。所以在这些支持预先载的浏览器中流程图应该是这样的:
g.js
test.js
为了利用预加载这个特性,我们可以使用如下的写法:
<script src="app1.js"></script> <script src="app2.js"></script> <script src="./test.js" async></script>
使用标准的script标签写法,确保浏览器能够正确的识别这是一个外链JS文件。同时设置async标签,浏览器便会异步加载test.js文件,不会暂停掉浏览器的解析执行。流程图如下:
这里有一个DEMO。
但它也并不完美,因为一些旧浏览器并不支持async属性。这会导致这个test.js文件在这些浏览器中不是异步的,并且会阻止掉页面渲染。有一个好消息是移动浏览器大多都支持async标签,如果你的用户大都是移动浏览器的,或者你的产品不支持旧浏览器,那么你可以使用这种方法。
当然如果你不介意第三方JS代码(本身也支持支持)被延后到页面解析完毕后执行,那么你可以再加上defer属性:
defer
<script src="./test.js" async defer></script>
Firefox支持defer属性要比支持async早一点点。而且当浏览器同时使用了async和defer属性之后,浏览器会忽略defer属性。所以可以放心的同时使用async和defer属性。对于不支持async的浏览器,会自动fallback到defer。不过支持程度也就多了一点点,Firefox的旧版占比已经很低了,基本可以忽略不计。
onload
上面提到的两种方法还有一个缺点:会影响到页面的onload事件。这对第一方JS可能没有影响,因为第一方JS大都是页面主要逻辑,从业务逻辑上来说它们的加载影响到页面onload事件触发不会有问题。但第三方JS则不一样,曾经因为Google被墙GA(Google Analytics简称)的加载就会特别慢甚至失败。导致了很多使用了GA的页面加载特别"慢",页面一直处于loading状态。大家先通过fiddler代理来设置test.js的加载时间为10秒,然后打开之前的DEMO,查看页面的loading是否会被延长。下面是我打开第一个DEMO的结果:
可以看到因为test.js的下载速度变慢,整个页面一直处于loading状态。页面的load事件要等到全部加载完成之后才会触发。如果页面中的主要逻辑是在页面load之后再执行,那么页面很可能会在很长一段时间内不可用。极大的影响了用户的使用体验。
load
为了解决这个问题,meebo的工程师想了一个方案来解决这个问题:
(function(url){ // 第一部分 var dom,doc,where,iframe = document.createElement('iframe'); iframe.src = "javascript:false"; iframe.title = ""; iframe.role="presentation"; (iframe.frameElement || iframe).style.cssText = "width: 0; height: 0; border: 0"; where = document.getElementsByTagName('script'); where = where[where.length - 1]; where.parentNode.insertBefore(iframe, where); // 第二部分 try { doc = iframe.contentWindow.document; } catch(e) { // IE下如果主页面修改过document.domain,那么访问用js创建的匿名iframe会发生跨域问题,必须通过js伪协议修改iframe内部的domain dom = document.domain; iframe.src="javascript:var d=document.open();d.domain='"+dom+"';void(0);"; doc = iframe.contentWindow.document; } doc.open()._l = function() { var js = this.createElement("script"); if(dom) this.domain = dom; js.id = "js-iframe-async"; js.src = url; this.body.appendChild(js); }; doc.write('<body onload="document._l();">'); doc.close(); })('test.js');
上述代码分为两个部分:
这样加载Javascript,就不会阻止浏览器的onload事件,提升普通用户的体验。还有另一个好处:第三方的Javascript代码在独立的iframe中运行,不会与主页面中的JS相互干扰。已经有了一些基于这个想法的开源实现,例如:lightning.js是一个专用于快速、安全、异步地加载第三方JS代码的库。
这个方法也不完美,它需要创建一个iframe标签导致了开销较大。同时还需要第三方JS本身的支持。第三方JS代码运行在iframe中,导致它无法获取到页面上的信息。虽然它并非跨域可以获得window.parent,但是第三方代码并不能知道自己是否在iframe中,需要在加载第三方JS代码的时候通知它。具体的通知方法千变万化,而第三方JS的内容又不受我们控制。
window.parent
富媒体广告JS(用于展示交互广告的JS)一般都会运行在隔离环境里面,且不需要(不允许)访问外部的window对象。如果你需要加载的第三方JS全部是广告时,那么使用这个方案是OK的,否则并不是最为合适。幸运的是有一个叫iAB(The Interactive Advertising Bureau,简称iAB)的组织,建立了一套工业级标准。虽然标准已经比较旧了,但是里面提到了通过设置变量inDapIF为true来通知第三方JS:你现在正运行在iframe中。因为iAB成员较多影响力大,所以遵循这个标准是有好处的,比起自己玩一套要好的多。
inDapIF
true
<script async src="test.js"></script>
<script async defer src="test.js"></script>
The text was updated successfully, but these errors were encountered:
不错的文章,感谢分享
Sorry, something went wrong.
配图太帅了
非常感谢分享这么精彩的文章,帮助很大。
很好的文章,想问一个问题,如果我代码这么写:
<script> setTimeout(function() { // 动态加载资源 loadScript('test.js') }, 0) </script>
这个会阻碍 onload 事件吗?
No branches or pull requests
从网站开发者的角度来看,第三方JS相比第一方JS有如下几个不同之处:
如果你的网站上面有很多第三方JS代码,那么“下载速度的不可控”很有可能导致你的网站会被拖慢。因为JS在执行的时候会影响到页面的DOM和样式等情况。浏览器在解析渲染HTML的时候,如果解析到需要下载文件的
script
标签,那么会停止解析接下来的HTML,然后下载外链JS文件并执行。等JS执行完毕之后才会继续解析剩下的HTML。这就是所谓的『HTML解析被阻止』。浏览器解析渲染页面的抽象流程图如下:第三方JS代码并不受网站开发者的控制,很有可能会出现加载时间长甚至加载失败的情况。这时候就会导致整个页面的加载速度变慢。第三方JS代码越多这种风险越大。按照互联网守则:
所以要考虑下如何在有很多第三方JS的情况下,保证他们不影响到网站自己的加载速度。我们可以异步加载这些第三方JS代码。
异步加载
异步加载JS的方法很多,最常见的就是动态创建一个
script
标签,然后设置其src
和async
属性,再插入到页面中。这里有个DEMO。实际操作的代码如下:PS:为了避免IE8以前版本的bug,并且确保script能插入DOM树,所以这里没有直接
document.body.append(src)
,而是调用了insertBefore
方法。改成异步加载第三方JS代码之后,在JS的下载过程中浏览器会继续解析渲染HTML。流程图就变成了如下:
因为
loadScript
的操作也是使用JS实现的,所以在JS下载之前会有一段执行JS代码的消耗。但是这段JS代码很简单,很快就会执行完毕。除了动态创建
script
标签的方法,异步加载JS的方法还有很多其他奇技淫巧,这里也罗列了一下:XMLHttpReqeust
对象或者JSONP
方法下载可执行的JS文件,然后使用eval()
或者script
标签执行JS。第三方JS文件一般是不同域名的且JS内容不可控,所以此方法就不适用了iframe
中加载JS – 将你的JS文件直接放到另一个页面的HTML中,然后将此页面URL地址作为iframe
标签src
属性。此方法需要增加一次页面请求,而且因为是在iframe
内部执行了,第三方JS文件本身也需要修改,故并不是很适用new Image().src = 'http://url.com/sample.js'
之类(或者Object
对象)的方法下载JS文件。然后在真正需要解析执行JS的时候下载(有缓存,不必再次下载)和执行JS文件。此方法不仅仅适用于JS文件,同样也可以用于CSS文件。这样我们就可以将静态文件的下载和解析执行(使用)分开,批量并行下载,然后在合适的机会解析执行(使用)。但此方法需要强缓存的配合,第三方JS为了在版本发布时更早的更新JS代码一般都不会设置缓存,甚至有些第三方JS的代码是服务器端动态生成的。所以也不是适用于第三方JS。浏览器预加载机制
动态创建
script
标签的方法可以异步加载第三方JS,但它也有缺陷。如果加载代码之前有外链JS文件或CSS文件需要下载,如下面的代码:那么会先下载解析
app1.js
和app2.js
再执行我们的loadScript
方法,所以第三方脚本的下载也会被暂停。流程图如下:而如今我们页面中代码如此复杂,触发这种case的情况很多。上面的DEMO中实际下载过程也确实是这样,动态创建
script
标签方式下载的test.js需要等到其他CSS和JS文件下载执行完毕之后才开始下载。如下图:虽然这对页面原有JS的执行不会有大的影响,但会影响到第三方JS代码本身的下载与执行。如何解决这个问题呢?
你可能已经发现上面的例子有个问题:HTML代码中
g.js
的位置在test.js
之后却先下载了。其实这得益于浏览器的预解析机制,会先对HTML代码做静态分析找到外链的JS和CSS文件,然后并行下载下来(但是执行顺序不变)。IE>=8 及其他主流浏览器基本都实现了这个功能。所以在这些支持预先载的浏览器中流程图应该是这样的:为了利用预加载这个特性,我们可以使用如下的写法:
使用标准的
script
标签写法,确保浏览器能够正确的识别这是一个外链JS文件。同时设置async
标签,浏览器便会异步加载test.js
文件,不会暂停掉浏览器的解析执行。流程图如下:这里有一个DEMO。
但它也并不完美,因为一些旧浏览器并不支持
async
属性。这会导致这个test.js
文件在这些浏览器中不是异步的,并且会阻止掉页面渲染。有一个好消息是移动浏览器大多都支持async
标签,如果你的用户大都是移动浏览器的,或者你的产品不支持旧浏览器,那么你可以使用这种方法。当然如果你不介意第三方JS代码(本身也支持支持)被延后到页面解析完毕后执行,那么你可以再加上
defer
属性:Firefox支持
defer
属性要比支持async
早一点点。而且当浏览器同时使用了async
和defer
属性之后,浏览器会忽略defer
属性。所以可以放心的同时使用async
和defer
属性。对于不支持async
的浏览器,会自动fallback到defer
。不过支持程度也就多了一点点,Firefox的旧版占比已经很低了,基本可以忽略不计。页面
onload
事件上面提到的两种方法还有一个缺点:会影响到页面的
onload
事件。这对第一方JS可能没有影响,因为第一方JS大都是页面主要逻辑,从业务逻辑上来说它们的加载影响到页面onload
事件触发不会有问题。但第三方JS则不一样,曾经因为Google被墙GA(Google Analytics简称)的加载就会特别慢甚至失败。导致了很多使用了GA的页面加载特别"慢",页面一直处于loading状态。大家先通过fiddler代理来设置test.js
的加载时间为10秒,然后打开之前的DEMO,查看页面的loading是否会被延长。下面是我打开第一个DEMO的结果:可以看到因为
test.js
的下载速度变慢,整个页面一直处于loading状态。页面的load
事件要等到全部加载完成之后才会触发。如果页面中的主要逻辑是在页面load
之后再执行,那么页面很可能会在很长一段时间内不可用。极大的影响了用户的使用体验。Friendly IFrame方法
为了解决这个问题,meebo的工程师想了一个方案来解决这个问题:
上述代码分为两个部分:
iframe
标签,设置其src
值为JS代码,然后插入到主页面中iframe
标签load之后加载JS脚本这样加载Javascript,就不会阻止浏览器的
onload
事件,提升普通用户的体验。还有另一个好处:第三方的Javascript代码在独立的iframe中运行,不会与主页面中的JS相互干扰。已经有了一些基于这个想法的开源实现,例如:lightning.js是一个专用于快速、安全、异步地加载第三方JS代码的库。这个方法也不完美,它需要创建一个
iframe
标签导致了开销较大。同时还需要第三方JS本身的支持。第三方JS代码运行在iframe中,导致它无法获取到页面上的信息。虽然它并非跨域可以获得window.parent
,但是第三方代码并不能知道自己是否在iframe中,需要在加载第三方JS代码的时候通知它。具体的通知方法千变万化,而第三方JS的内容又不受我们控制。富媒体广告JS(用于展示交互广告的JS)一般都会运行在隔离环境里面,且不需要(不允许)访问外部的window对象。如果你需要加载的第三方JS全部是广告时,那么使用这个方案是OK的,否则并不是最为合适。幸运的是有一个叫iAB(The Interactive Advertising Bureau,简称iAB)的组织,建立了一套工业级标准。虽然标准已经比较旧了,但是里面提到了通过设置变量
inDapIF
为true
来通知第三方JS:你现在正运行在iframe中。因为iAB成员较多影响力大,所以遵循这个标准是有好处的,比起自己玩一套要好的多。总结
onload
事件script
标签<script async src="test.js"></script>
<script async defer src="test.js"></script>
The text was updated successfully, but these errors were encountered: