关键在于:当您从第三方加载 JavaScript 时,应该异步加载。 您可能也希望异步加载自己的脚本,但本文我们将重点关注第三方脚本。
这样做的原因有两个:
- 如果第三方服务宕机或速度缓慢,您的页面不会因等待加载该资源而被阻塞。
- 它可以加快页面加载速度。
在 Wufoo,我们 刚刚切换到 异步嵌入代码片段。现在建议使用 Wufoo 构建表单并将其嵌入到网站的用户使用此方法。我们正是出于上述原因才这样做的。对于一个要求用户链接到该服务网站上的资源的网络服务来说,这是负责任的做法。
让我们来探索一下这个异步加载。
嗯,什么?
这里涉及一些术语,可以帮助我们理解“异步”这个总称。
“解析器阻塞” – 浏览器读取您的 HTML,当遇到 <script>
标签时,它会先下载整个资源,然后再继续解析。这肯定会减慢页面加载速度,尤其是在脚本位于头部或任何其他可视元素的上方时。这在旧版浏览器以及现代浏览器中都是如此,前提是您没有使用 async
属性(稍后详细介绍)。来自 MDN 文档:“在不支持 async
属性的旧版浏览器中,解析器插入的脚本会阻塞解析器……”
为了防止出现有问题的解析器阻塞,可以“脚本插入”(即使用 JavaScript 插入另一个脚本),这将强制它们异步执行(Opera 或 4.0 之前的 Firefox 除外)。
“资源阻塞” – 在下载脚本期间,它可能会阻止其他资源同时下载。IE 6 和 7 会这样做,一次只允许下载一个脚本,并且不允许下载任何其他资源。IE 8 和 Safari 4 允许并行下载多个脚本,但会阻塞任何其他资源(参考)。
理想情况下,我们要解决这两个问题,并加快页面加载速度(实际和感知速度)。
HTML5 方法
HTML5 中的 script
标签有一个 async
属性(规范)。示例:
<script async src="https://third-party.com/resource.js"></script>
它的 浏览器支持情况 是 Firefox 3.6+、IE 10+、Chrome 2+、Safari 5+、iOS 5+、Android 3+。Opera 尚未支持。
如果您要直接加载脚本,使用此属性可能是一个好主意。它可以防止解析器阻塞。这些较新的浏览器在资源阻塞方面没有太大问题,但解析器问题是一个大问题。我们在 Wufoo 中没有使用它,因为我们需要比这更广泛的浏览器支持。
经典的异步方法
这是一个基本的异步脚本加载模式,它将为您提供所需的广泛浏览器支持。
<script>
var resource = document.createElement('script');
resource.src = "https://third-party.com/resource.js";
var script = document.getElementsByTagName('script')[0];
script.parentNode.insertBefore(resource, script);
</script>
这是一个更高效的版本,它使用一个包装器来封装这些变量(鸣谢 Mathias Bynens)
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = 'https://third-party.com/resource.js';
s.parentNode.insertBefore(g, s);
}(document, 'script'));
async
属性。广告网络
BuySellAds 是一个广告网络,它是 最早 异步投放广告的网络之一。这是它们的模式:
<script type="text/javascript">
(function(){
var bsa = document.createElement('script');
bsa.type = 'text/javascript';
bsa.async = true;
bsa.src = 'https://s3.buysellads.com/ac/bsa.js';
(document.getElementsByTagName('head')[0]||document.getElementsByTagName('body')[0]).appendChild(bsa);
})();
</script>
这里需要注意两点:
- 他们在附加脚本之前将脚本的
async
属性设置为 true。这仅对 Firefox 3.6 有用,因为它是唯一一个默认情况下不执行此操作的浏览器。在大多数情况下,这可能可以省略。设置脚本type
绝对没有必要。 - 与上面的简单模式一样,
src
使用 协议相对 URL 设置。这是一种非常有用的方法,可以根据请求页面的不同,从 HTTP 或 HTTPS 加载脚本。在 Wufoo,我们绝对需要这样做,但不幸的是,我们发现它在 IE 6 中使用默认安全设置时会引发错误。如果您不需要 IE 6 支持,请随时使用它。 - 他们将脚本附加到头部或主体,以先找到哪个为准。这是一种完全可以接受的方法,但查找脚本元素也同样安全,因为代码本身就是一个脚本元素。
The Deck 使用不同的模式:
<script type="text/javascript">
//<![CDATA[
(function(id) {
document.write('<script type="text/javascript" src="' +
'http://connect.decknetwork.net/deck' + id + '_js.php?' +
(new Date().getTime()) + '"></' + 'script>');
})("DF");
//]]>
</script>
我非常确定这仍然算作异步加载,因为他们加载的资源是脚本插入的脚本,这意味着它不会阻塞解析器。
如果需要回调函数怎么办?
有时您需要加载第三方脚本,然后在加载该脚本后,触发一些自定义代码。该自定义代码可能会调用第三方脚本中定义的一些函数,并使用特定于某个页面的数据。
<script src="https://third-party.com/resource.js"></script>
<script>
doSomethingFancy('chriscoyier');
</script>
Typekit 就处于这种情况。您需要加载 Typekit JavaScript,然后启动它。Typekit 实际上是利用了脚本阻塞解析器的事实。如果您的页面被阻塞,直到他们的脚本加载完毕,您将看不到“FOUT”(未设置样式文本的闪烁),这通常只在 Firefox 中会出现问题,但在 Typekit 中也存在问题,因为 @font-face 资源是通过 JavaScript 加载的。
<script type="text/javascript" src="https://use.typekit.com/abc1def.js"></script>
<script type="text/javascript">try{Typekit.load();}catch(e){}</script>
这很聪明,但也略微危险。如果 Typekit 出现故障或速度缓慢:“曾经为了隐藏 FOUT 而延迟渲染变成了一个严重的问题,当脚本加载时间超过几秒钟时。”(参考)。
以下是如何以异步方式加载 Typekit 的方法:
<script type="text/javascript">
TypekitConfig = {
kitId: 'abc1def'
};
(function() {
var tk = document.createElement('script');
tk.src = 'https://use.typekit.com/' + TypekitConfig.kitId + '.js';
tk.type = 'text/javascript';
tk.async = 'true';
tk.onload = tk.onreadystatechange = function() {
var rs = this.readyState;
if (rs && rs != 'complete' && rs != 'loaded') return;
try { Typekit.load(TypekitConfig); } catch (e) {}
};
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(tk, s);
})();
</script>
处理 onload 回调函数需要大量复杂的代码。不幸的是,这就是使用广泛浏览器支持的回调函数的方法。请注意,使用此模式实际上会带来 FOUT 问题。如果您想使用 Typekit 的异步加载,并获得与平时一样好的体验,请 阅读他们的文章,其中介绍了一些巧妙的类名操作和字体事件。
jQuery 和其他脚本加载器
如果您已经在使用 jQuery,加载第三方脚本并在其准备好后获取回调非常容易:
$.ajax({
url: 'https://third-party.com/resource.js',
dataType: 'script',
cache: true, // otherwise will get fresh copy every page load
success: function() {
// script loaded, do stuff!
}
}
我相信其他库也有类似的功能。这是 JavaScript 库擅长帮助解决的经典问题。另请参阅 getScript,它可能更简洁一些。
如果您没有使用库,并且担心文件大小,YepNope 是一个超小的脚本加载器,也可以提供帮助。它的理想用途是执行测试以查看是否需要加载脚本,但它也具有直接方法:
yepnope.injectJs("https://third-party.com/resource.js", function () {
// script loaded, do stuff!
});
相关:Max Wheeler 的文章 使用 YepNope 异步加载 Typekit。
您真的需要回调函数吗?
在 Wufoo,我们认为是的,我们需要回调函数。我们需要加载表单嵌入 JavaScript 资源,然后使用用户表单的所有详细信息调用一个函数。这就是我们过去使用的方法:
<script type="text/javascript">var host = (("https:" == document.location.protocol) ? "https://secure." : "http://");document.write(unescape("%3Cscript src='" + host + "wufoo.com/scripts/embed/form.js' type='text/javascript'%3E%3C/script%3E"));</script><br />
<script type="text/javascript">
var z7p9p1 = new WufooForm();
z7p9p1.initialize({
'userName':'chriscoyier',
'formHash':'z7p9p1',
'autoResize':true,
'height':'546',
'ssl':true});
z7p9p1.display();
</script>
这组键值对对用户很有用,他们可以查看、更改和添加内容。我们尝试了一些方法来保留它,但将这些数据作为 URL 的一部分传递,以便在调用脚本时使用。我们这边的脚本实际上是 PHP,并且能够 $_GET
这些值。这样就可以避免在异步模式下处理所有难看的回调代码。可能类似于:
script.src = 'https://wufoo.com/form.js?data=' + JSON.stringify(options);
但最终,我们放弃了这种方法。回调代码并不糟糕,JSON 字符串化没有得到很广泛的浏览器支持(在复制粘贴的代码中包含 polyfill 也不切实际),并且我们的 form.js 缓存效果很好。
社交媒体
社交媒体按钮是一个经典的需要在页面上使用第三方 JavaScript 的案例。有趣的是,三大巨头已经以异步模式提供了他们的代码。
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "https://#/en_US/all.js#xfbml=1&appId=200103733347528";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<a href="https://twitter.com/share" class="twitter-share-button">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
Google Plus
<g:plusone annotation="inline"></g:plusone>
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
清理代码
以上所有代码都非常相似,但也略有不同。如果直接将它们全部放在页面上,可能会让像我们这样的代码洁癖者感到崩溃。Nicholas Gallagher 有一个非常简洁高效的方法将它们整合在一起。
(function(doc, script) {
var js,
fjs = doc.getElementsByTagName(script)[0],
add = function(url, id) {
if (doc.getElementById(id)) {return;}
js = doc.createElement(script);
js.src = url;
id && (js.id = id);
fjs.parentNode.insertBefore(js, fjs);
};
// Google Analytics
add('https:://#/ga.js', 'ga');
// Google+ button
add('https://apis.google.com/js/plusone.js');
// Facebook SDK
add('https://#/en_US/all.js', 'facebook-jssdk');
// Twitter SDK
add('https://platform.twitter.com/widgets.js', 'twitter-wjs');
}(document, 'script'));
处理 CMS
WordPress 非常庞大。其他主要的 CMS 也是如此。当您作为第三方提供复制粘贴的 JavaScript 代码时,不能忽略它们。关键当然在于测试。最重要的是不要在代码中包含双换行符。例如
<script type="text/javascript">
var s = d.createElement(t), options = {
foo: bar
}
// The line break space above is bad!
</script>
这看起来可能很简洁,但 WordPress 的“自动”行为会在其各个部分插入段落标签,这当然会阻止脚本按预期执行。
最终的 Wufoo 代码片段
这就是我们最终得到的代码。
<div id="wufoo-z7w3m7">
Fill out my <a href="http://examples.wufoo.com/forms/z7w3m7">online form</a>.
</div>
<script type="text/javascript">var z7w3m7;(function(d, t) {
var s = d.createElement(t), options = {
'userName':'examples',
'formHash':'z7w3m7',
'autoResize':true,
'height':'260',
'async':true,
'header':'show',
'ssl':true};
s.src = 'https://wufoo.com/scripts/embed/form.js';
s.onload = s.onreadystatechange = function() {
var rs = this.readyState; if (rs) if (rs != 'complete') if (rs != 'loaded') return;
try { z7w3m7 = new WufooForm();z7w3m7.initialize(options);z7w3m7.display(); } catch (e) {}};
var scr = d.getElementsByTagName(t)[0], par = scr.parentNode; par.insertBefore(s, scr);
})(document, 'script');</script>
老实说,代码片段的“大小”是一个问题。50 行对于这样的代码来说实在太多了。现在是 19 行,比我们之前多,但可以接受。其中很多行是 options 对象,我们可以将其压缩成更少的行,但将其分别放在单独的行中以方便阅读和修改会更好。
我们仍然需要支持 IE 6,因此不幸的是我们不能使用协议相对 URL。我们正在使用 location.protocol
测试。
它比您的“普通”异步代码片段(如果存在“普通”代码片段的话)要大一些,但这没关系。它需要完成很多工作,并且做得很好。
我们在公告博文中讨论了其中的一些优势。我最喜欢的一点是,您现在可以将脚本移动到任何您想要的位置,它不必像以前那样必须完全位于您希望表单出现的位置。
瀑布图
如果您有兴趣对资源加载进行一些测试,查看资源瀑布图特别有用。现代的 Web 开发工具内置了此功能,例如 Web 检查器中的“网络”选项卡或 Firebug 的“网络”选项卡。但是 IE 6-8 中的旧版开发工具不提供这些信息。幸运的是,网站Web Page Test提供了这些信息(它有点丑,但非常酷)。
在为 IE 6 中的 Wufoo 代码片段进行一些测试时,我可以证明我们的新方法是非阻塞的,而旧方法是阻塞的。
好了,这就是我所要说的。写下所有这些东西让我感觉有点奇怪,因为这些东西对我来说都是相当新的,而且我觉得自己离专家还差得远。所以,请随时纠正我的任何错误或分享您自己的异步体验。
defer
属性在所有这些中扮演什么角色?它立即开始(非阻塞,对吧?)下载,但会在页面解析完成之前等待执行,如果我没记错的话,它还有按顺序执行的优点。另外,我可能错了,但我认为它避免的一个问题是执行会导致解析(和/或可能重新流?)暂停……似乎在解析过程中加载一个大型/复杂的
<script async>
会导致一些问题。defer
的浏览器支持非常糟糕。IE 引入了它,但即使是 IE8 也不总是按正确的顺序运行延迟脚本。其他现代浏览器对它的支持有限、有错误或根本不支持。参考
mozilla.org
sitepoint.com
改为使用
window.onload()
(或库函数,例如 jQuery 的$(document).ready()
方法)。Chris,你提到了 YepNope,但我更习惯使用 LABjs 的脚本:http://labjs.com/(现在有很多这样的脚本)。
哇,我以前根本不知道这个!谢谢 :)
我对它抛出的错误类型以及您如何测试这一点很感兴趣。
在维基百科,我们从 2011 年秋季/冬季开始将几乎所有内容都切换到协议 URL,并尽可能支持 IE6。我不记得收到任何警告(在 http 和 https 上都进行了测试)。
您如何看待这个用于并行加载 js 的脚本:http://headjs.com/
我认为这也是一种很好的 js 加载方式。
我也非常喜欢 headjs,即使有时很难让它按预期工作。
对于异步脚本加载,我最近转向了RequireJS,我已经在几个项目中使用了它,并且发现它在查看网络结果时非常一致。
Chris,我很惊讶你没有提到 Require.js!你为什么选择不提它?
有人可以帮我用 Nicolas Gallagher 的异步社交按钮加载方法吗?我需要在脚本中加入 async=1,但不知道什么时候会破坏它。
https://gist.github.com/1025811
@Teddy Cross 如果你对
defer
属性感兴趣,我强烈建议你查看 Paul Irish 的延迟 Web 请求 - Issue #42 中的讨论。https://github.com/paulirish/lazyweb-requests/issues/42
如果你不想点击链接。Paul 用以下明智的话总结了它
希望这有帮助。
哦,还有很棒的文章 Chirs!
嗨,Chris,
实际上不是,如果你以这种方式使用
document.write
插入一个脚本元素,解析器确实会阻塞新添加的脚本元素。这是一种非常常见的添加更多阻塞脚本的方法。你可以在这里尝试(源代码)。最好的祝愿,
—— T.J.
你知道是否可以异步加载 Google Adsense 吗?我上周尝试了几次使用 window.onload,但完全无法正常工作。
如果你使用的是当前的广告代码,它已经是异步的了。
http://www.webmaster-source.com/2011/03/18/google-rolling-out-asynchronous-adsense-ads/
但它只有在首先同步加载了 show_ads.js 脚本之后才是异步的。
我认为需要提到的是,包含
document.write()
的 JavaScript(在处理第三方脚本时非常常见)无法异步加载。至少在没有黑客手段的情况下。例如,The Deck 示例不会异步渲染,因为该脚本使用
document.write()
注入广告。加载和解析会被阻塞。这就是为什么像 Daring Fireball 这样的网站在 The Deck 网络出现故障时无法渲染的原因。这是一个演示页面,显示了当使用
document.write()
插入包含document.write()
的脚本时会发生什么。响应休眠 3 秒以演示阻塞。http://www.ravelrumba.com/code/demos/blocking-ads/demo-blocking2.html
总之,重点是需要与你加载的任何外部脚本进行一些协作,以确保异步性。
hinclude.js 是一个用于包含异步内容的优秀库。
http://mnot.github.com/hinclude/
document.write 邪恶……但它似乎仍然并行加载,尽管有一些问题:http://stevesouders.com/cuzillion/?ex=10014
非常棒的文章,谢谢!!尤其是在 Panda 和 Cafeine 更新之后,页面加载速度比以往任何时候都更加重要……页面已添加书签 ;-)
喜欢这篇文章。Typekit 的加载方式一直看起来像是危险的脚本。我一直使用 async(非常类似于你上面的代码片段)来加载它,但肯定仍然会遇到 FOUT。我喜欢他们文章中关于类操作以帮助解决此问题的链接。好东西,谢谢 Chris!
非常感谢这篇综述!我制作了很多东西被注入到客户端站点中,并且我正开始将它们全部更新为异步,这要归功于这篇文章!
“有两个原因导致这种情况:”
rawr。语法。喵。
需要注意的一件事是,Modernizr 实际上包含了 YepNope 脚本加载器,这很棒,因为我已经为 HTML5 回退和功能检测加载了 Modernizr。
嘿,Chris,
感谢分享。
jQuery 也拥有 getScript 函数,它是 .ajax 示例的简写。 https://api.jqueryjs.cn/jQuery.getScript/
不想吹毛求疵。你上面处理 CMS 的示例
从 foo:bar; 中删除分号。
谢谢
你也可以使用回调,就像 Google Maps API 所做的那样
有点唠叨,但经典方法需要一个结束标签
谢谢
Chris,对于 jQuery 用户来说,值得一提的是,使用异步 JS 会影响 $(window).load(function(){});,这对于在页面上的所有图像加载完成后触发 DOM 操作很常见。第三方异步调用的次数越多,对窗口加载事件进行超时或错误处理的需求就越大。
嗨,Chris,
我认为你的 WuFoo 新异步代码片段有改进的空间,特别是在代码大小方面。
我重写了代码片段的某些部分,保持了相同的功能。
看看这个 gist:https://gist.github.com/2011869
希望你喜欢;)
你知道是否可以异步加载 Google Adsense 吗?我上周尝试了几次使用 window.onload,但完全无法正常工作。
我认为所有 h5bp 粉丝都不必担心阻塞脚本,因为他们可能在他们的 html-head 中包含了 modernizr,从而可以使用优秀的
Modernizr.load
方法。这实际上使得所有其他脚本加载器都变得多余。除了 require.js。但这不仅仅是一个简单的脚本加载器,而是一种模块化 JavaScript 和依赖项管理的方法。不需要 require.js 仅仅是为了异步加载一两个外部脚本。
感谢分享,一如既往地感谢。
干杯,
Chris
嘿,Chris,
jQuery 代码片段非常简洁!
但你可能需要添加
cache: true
,因为 jQuery 默认情况下通过附加查询字符串避免为dataType: script
缓存。在大多数情况下,这会导致客户端/服务器端性能问题,因此你可能需要为那些不每天阅读文档的 jQuery 用户添加提示;)
我们在最近的几个项目中愉快地使用了 head.js。- 它可预测且易于理解。
对于 jQuery,你应该使用
$.getScript()
函数。就实际发生的事情而言,它使代码更易于阅读。不错的概述,Chris。
对于基于 jQuery 的示例,最好通过指定
'cache': true
来启用缓存,如下所示:https://gist.github.com/1197140 否则,将向请求 URL 附加一个带时间戳的查询参数,以确保浏览器每次请求时都下载脚本- 在这种情况下可能不是你想要的。值得注意的是,
jQuery.getScript()
默认情况下也会清除缓存。唯一可以覆盖此方法(除了使用jQuery.ajax()
并启用cache
选项之外)的方法是全局设置cache
属性有一个脚本可以让你解决 document.write 问题。它被称为 Ghostwriter:http://digital-fulcrum.com/solutions/ghostwriter-complete-control/
以异步方式加载外部资源是必须的!
在某些项目中,我们甚至异步加载 CSS……
显然,我们异步加载仅在用户执行“操作”后或 CSS 包含某些可选(例如 codemirror css)HTML 元素规则时才需要的样式表。
我们使用“标准方式”来加载异步 js/css,即将脚本/链接元素“附加”到头部。
顺便说一句,感谢这篇文章!
谁会想到还在为 IE6 而烦恼?!忘记它吧。
为什么不直接将脚本放在 body 标签的底部?除非你的用户正在下载大量的脚本文件数据(1MB+),否则页面几乎会立即响应,并且脚本会在用户尝试执行任何操作之前加载(因为 DOM 和样式已经加载)。哎呀,它实际上会加载得更快,因为用户不需要等待额外的 HTTP 请求来获取文件。这就是我们不再切分图像的原因 :)
我想对于多个、更大、不太重要的文件,如果它们明显阻碍了网站性能,这样做是有意义的。对我来说,让用户等待异步加载管理器加载,并在其之上等待脚本异步加载,这似乎很奇怪。
jQuery 的示例这里略有错误,以下是小的修改
几周前我做过类似的事情。我喜欢以“Facebook”的方式进行回调部分,也检查脚本是否已加载(但这部分当然可以移除)
你可以在 Github 上查看其余部分
https://github.com/keriati/async-js
Chris,总结得很好!
对于像“Tweet”和“Like”这样的社交按钮,你可能会喜欢 Socialite.js http://socialitejs.com ——我正在开发一个专门用于社交网络的脚本加载器,它可以无限期地延迟加载。
感谢这篇文章。
在 Google Page Speed Online 建议我异步加载脚本后,我偶然发现了你的帖子。
你也可以使用
<script id="blah">
+getElementById('blah').addEventListener('load')
的方式在异步脚本执行后执行某些操作。像 RequireJS 这样的库可以处理并行加载和最终的回调事件,以避免复杂的嵌套代码。
这很好!
但是我们无法检测 Google 地图 API 中的 JS 是否加载以执行我们的回调函数。
Google 总是会在他们的 API 代码中加载多个 javascript 文件…
让我们回顾一下 https://maps.googleapis.com/maps/api/js?sensor=false
谢谢 Chris!
你尝试过 Socialite.js https://github.com/tmort/Socialite 吗?——对于社交网络也很不错
我认为最有趣的异步脚本加载器之一,没有人谈论它,那就是 defer.js。与任何其他解决方案不同,它可以轻松地异步加载自身。这意味着零阻塞脚本。所有你的“必须执行”代码可以放在 CSS 之前,就像现在建议的那样。
我更喜欢为所有自托管 WordPress 用户使用插件。插件名称为:Async JS and CSS。目前我正在使用它,Google 页面速度显示为空的渲染阻塞 Java 脚本和 CSS。对我来说效果很好……