专栏首页Golang语言社区【前端编程】加载第三方JS的各种姿势

【前端编程】加载第三方JS的各种姿势

从网站开发者的角度来看,第三方JS相比第一方JS有如下几个不同之处:

  • 下载速度不可控
  • JS地址域名与网站域名不同
  • 文件内容不可控
  • 不一定有强缓存(Cache-Control/Expires)

如果你的网站上面有很多第三方JS代码,那么“下载速度的不可控”很有可能导致你的网站会被拖慢。因为JS在执行的时候会影响到页面的DOM和样式等情况。浏览器在解析渲染HTML的时候,如果解析到需要下载文件的script标签,那么会停止解析接下来的HTML,然后下载外链JS文件并执行。等JS执行完毕之后才会继续解析剩下的HTML。这就是所谓的『HTML解析被阻止』。浏览器解析渲染页面的抽象流程图如下:

第三方JS代码并不受网站开发者的控制,很有可能会出现加载时间长甚至加载失败的情况。这时候就会导致整个页面的加载速度变慢。第三方JS代码越多这种风险越大。按照互联网守则:

网站加载速度越慢,用户流失越多

所以要考虑下如何在有很多第三方JS的情况下,保证他们不影响到网站自己的加载速度。我们可以异步加载这些第三方JS代码。

异步加载

异步加载JS的方法很多,最常见的就是动态创建一个script标签,然后设置其src和async属性,再插入到页面中。这里有个DEMO。实际操作的代码如下:

<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方法。

改成异步加载第三方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文件本身也需要修改,故并不是很适用
  • 先缓存再执行 – 利用JS文件的强缓存,先使用new Image().src = 'http://url.com/sample.js'之类(或者Object对象)的方法下载JS文件。然后在真正需要解析执行JS的时候下载(有缓存,不必再次下载)和执行JS文件。此方法不仅仅适用于JS文件,同样也可以用于CSS文件。这样我们就可以将静态文件的下载和解析执行(使用)分开,批量并行下载,然后在合适的机会解析执行(使用)。但此方法需要强缓存的配合,第三方JS为了在版本发布时更早的更新JS代码一般都不会设置缓存,甚至有些第三方JS的代码是服务器端动态生成的。所以也不是适用于第三方JS。

浏览器预加载机制

动态创建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方法,所以第三方脚本的下载也会被暂停。流程图如下:

而如今我们页面中代码如此复杂,触发这种case的情况很多。上面的DEMO中实际下载过程也确实是这样,动态创建script标签方式下载的test.js需要等到其他CSS和JS文件下载执行完毕之后才开始下载。如下图:

虽然这对页面原有JS的执行不会有大的影响,但会影响到第三方JS代码本身的下载与执行。如何解决这个问题呢?

你可能已经发现上面的例子有个问题:HTML代码中g.js的位置在test.js之后却先下载了。其实这得益于浏览器的预解析机制,会先对HTML代码做静态分析找到外链的JS和CSS文件,然后并行下载下来(但是执行顺序不变)。IE>=8 及其他主流浏览器基本都实现了这个功能。所以在这些支持预先载的浏览器中流程图应该是这样的:

为了利用预加载这个特性,我们可以使用如下的写法:

  1. <script src="app1.js"></script>
  2. <script src="app2.js"></script>
  3. <script src="./test.js" async></script>

复制代码

使用标准的script标签写法,确保浏览器能够正确的识别这是一个外链JS文件。同时设置async标签,浏览器便会异步加载test.js文件,不会暂停掉浏览器的解析执行。流程图如下:

这里有一个DEMO。

但它也并不完美,因为一些旧浏览器并不支持async属性。这会导致这个test.js文件在这些浏览器中不是异步的,并且会阻止掉页面渲染。有一个好消息是移动浏览器大多都支持async标签,如果你的用户大都是移动浏览器的,或者你的产品不支持旧浏览器,那么你可以使用这种方法。

当然如果你不介意第三方JS代码(本身也支持支持)被延后到页面解析完毕后执行,那么你可以再加上defer属性:

  1. <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之后再执行,那么页面很可能会在很长一段时间内不可用。极大的影响了用户的使用体验。

Friendly IFrame方法

为了解决这个问题,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');

上述代码分为两个部分:

  • 创建了一个隐藏的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成员较多影响力大,所以遵循这个标准是有好处的,比起自己玩一套要好的多。

总结

本文分享自微信公众号 - Golang语言社区(Golangweb)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-07-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go语言并发编程总结

    Golang :不要通过共享内存来通信,而应该通过通信来共享内存。这句风靡在Go社区的话,说的就是 goroutine中的 channel …….

    李海彬
  • epoll入门

    epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数: 所用到的数据结构 typedef union epol...

    李海彬
  • Golang RPC 之 gRPC

    gRPC 简介: gRPC 是一款高性能、开源的 RPC 框架,产自 Google,基于 ProtoBuf 序列化协议进行开发,支持多种语言(Golang、Py...

    李海彬
  • 加载第三方JS的各种姿势

    如果你的网站上面有很多第三方JS代码,那么“下载速度的不可控”很有可能导致你的网站会被拖慢。因为JS在执行的时候会影响到页面的DOM和样式等情况。浏览器在解析渲...

    mmzhou
  • 大周末的不多说,面试十点必看

    实事求是的讲,前端新人因为时间的关系,对于JavaScript的理解确实是无法面面具到,甚至有些知识点我讲的比较深入,但因为每个人的程度不同,依然无法做到当堂理...

    web前端教室
  • WKWebView的使用与JS交互详细解读

    前言:     WKWebView 这是在iOS8.0之后增加的一个比UIWebView更加完善和强大的控件!看网上关于它的博客也是有许多的了,从各个方面总结一...

    Mr.RisingSun
  • 浅谈CDN、SEO、XSS、CSRF

    CDN 什么是CDN 初学Web开发的时候,多多少少都会听过这个名词->CDN。 CDN在我没接触之前,它给我的印象是用来优化网络请求的,我第一次用到CDN的时...

    Java3y
  • Web 前端性能优化概要

    本文遵守创作共享CC BY-NC-SA 4.0协议 网络平台如需转载必须与本人联系确认。

    Joel
  • 进程和线程的区别和联系

    我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功...

    葆宁
  • 自己动手用electron+vue开发博客园文章编辑器客户端【二】

    在nwjs里,宿主页面与做过特殊标记的iframe页面(nwdisable nwfaketop和nwUserAgent)通信,并没有什么特别的地方

    liulun

扫码关注云+社区

领取腾讯云代金券