脚本加载技术的过去,现在与未来

原文链接: http://www.stevesouders.com/blog/2010/12/06/evolution-of-script-loading/

Velocity China明天早晨即将拉开帷幕,我会做一个主题发言,而现在正在准备相关PPT(演示文稿)。届时我会做一些有关渐进增强和JavaScript智能加载的分享。就在复核PPT时,我惊讶地发现最近几年外部脚本(本文特指JavaScript)加载技术已发生了重大变化。现在就让我们一同来回顾过去、把握现在、畅想未来。

将脚本置于文档HEAD中

就在几年前,包括许多顶级网站在内的大部分网站都将外部脚本置于HTML文档的HEAD标签中:

<head>
<script src="core.js" type="text/javascript"></script>
<script src="more.js" type="text/javascript"></script>
</head>

熟悉web性能最佳实践的开发者一定会对以上方式深恶痛绝。我们都知道老式浏览器(主要是IE6、IE7)是按顺序加载脚本的。浏览器先加载core.js、然后解析core.js,最后执行它。接着对more.js重复以上过程:加载、解析,然后执行。

老式浏览器除了会按顺序加载脚本外,加载过程中还会阻塞其他资源的正常载入。这意味着老式浏览器在开始加载页面中的样式文件、图片以及其他资源前会出现明显的延迟。

这些加载问题在较新的浏览器中有所缓解(参见我的文章《浏览器脚本加载综述》)。 从IE8、Firefox3.6、Chrome2以及Safari4开始,脚本资源大体上可以做到与其他资源并行下载。注意,我用“大体上”源于浏览器间 在处理并行加载脚本上仍然存在不统一现象。例如,在IE8和IE9 beta 上,脚本下载期间图片的正常加载会被阻塞(例子)。一个更费解的案例是,Firefox3会按顺序加载通过document.write引入的脚本,而不是并行加载。

其 次,脚本也会阻塞页面的渲染:一切在SCRIPT标签之后的DOM元素只有等到脚本加载完成才会被渲染。浏览器已经把HTML文档下载好了,假如有一个 SCRIPT在P、DIV或是UL等之前,用户却不能立即看到内容。这真悲剧。如果将SCRIPT置于HEAD标签中,则整个页面的渲染都会被阻塞。

浏览器间(特别是老式浏览器)在加载脚本上还有很多细微差别。当完全理解了影响页面加载的各种因素后,我提出了第一套有关改善脚本加载的建议:

将脚本移至底部

回到2006年、2007年期间,那时候的我刚开始如何让脚本加载更快的研究,面对脚本加载时,所有浏览器都面临同样的问题:

  • 按顺序加载脚本;
  • 当一个脚本正在加载时会阻塞页面上所有其他资源的加载;
  • SCRIPT标签之后的内容只有等到该脚本加载完成后才会渲染。

我当时提出的解决方案是将SCRIPT标签移至页面底部。

...
<script src="core.js" type="text/javascript"></script>
<script src="more.js" type="text/javascript"></script>
</body>

并非什么时候都可以这样做,例如,通过document.write引入的广告代码就不能顺便移动,因为必须在广告预期出现的地方执行 document.write。不过许多脚本可以比较轻松地移到页面底部。好处是显而易见的:图片加载更快、页面渲染也更快。这也是我的书《高性能网站建设指南》中14条规则中的一条。很多网站采纳了这条建议并且也看到了收益。

异步加载脚本

将脚本移至页面底部确实避免了一些问题,但其他脚本加载问题仍然存在。2008到2009年期间,浏览器仍然是按顺序加载脚本文件。这有明显的提升性能的机 会。虽然脚本(通常)需要按顺序执行,但并不需要按顺序加载。在浏览器保证按原先顺序执行的前提下脚本可以以任意顺序加载。

浏览器提供商意识到了这一点。(我自恋地认为本人在此事上做了少许贡献。)较新的浏览器(如前所述,从IE8、Firefox3.6、Chrome2以及 Safari4开始)也开始并行加载脚本了。但回到2008、2009年期间,顺序加载脚本仍然还是一个问题。一天,我在分析MSN.com时注意到即使是在Firefox2.0上脚本也能并行加载。他们使用的是动态添加SCRIPT标签的技巧:

var se = document.createElement("script");
se.src = "core.js";
document.getElementsByTagName("head")[0].appendChild(se);

最近几年我花了大部分时间来研究像这样的脚本异步加载技术。这些异步技术(总结在此文里,具体细节参看《高性能网站建设进阶指南》)汇总了老式浏览器中脚本并行加载的办法以及如何避免较新浏览器中的一些怪异特性。它们也一定程度上缓解了阻塞渲染问题:当一个脚本文件通过异步方式载入页面时,浏览器响应加快,且在加载过程中能够正常渲染页面。在这个例子中,有一个通过动态添加SCRIPT标签技术放入HEAD中的脚本,此脚本被设置为4秒下载完成。当你打开例子页面的URL你会看到页面立即渲染,这证实了脚本异步加载过程中渲染可以执行。

更多的并行下载,更快的页面渲染。我们还可以期望什么?呃⋯⋯

异步加载 + 按需执行

异步加载脚本文件提升了下载速度(更多的并行数)和渲染速度。然而当脚本下载完成后就开始解析和执行,脚本解析和执行期间浏览器停止渲染、UI被锁住。如果所有的JavaScript都需要立即执行,那这里没有一点改善空间,然而网站并没有完全使用所下载的代码(至少没有立即使用)。Alexa 美国前10的网站平均会加载229KB(压缩后)的JavaScript,而在load事件触发前这些代码的函数仅仅执行了29%。其他的71%是无用的代码、条件分支、或者很可能是些初始页面时不需要的DHTML和Ajax函数。

此发现引导我提出了将JavaScript拆分的建议:一部分用于初始加载时渲染页面的代码;另一部分是可以稍后加载的用于DHTML和Ajax的代码(参见此文或是《高性能网站建设进阶指南》的第三章)。网站通常在window的onload事件处理函数中下载页面稍后需要的代码。Gmail Mobile team 发现 当后续代码到达浏览器时UI会被锁住,这是不恰当的。总之,这些用于DHTML/Ajax的代码甚至很可能不会被用到。他们是我发现的第一批找到方法分离脚本下载期和脚本解析-执行期的家伙。他们使用的方法是对所有代码加上注释,然后当需要时再移除注释分隔符并且执行它们。GMail的技术使 用了iframe,所以需要改变你现存的代码。Stoyan已经开始探索使用image和object标签加载脚本以避免执行他们,只有到需要执行时再进行专门的解析和执行。

接下来做什么?

web 页面变得越发复杂。关注性能问题的高级开发者对页面加载需要有更强的控制力。当外部脚本被解析和执行时给予开发者控制权是明智的。目前这并非易事。很幸运,我们已经看到曙光,浏览器对LINK的REL=”PREFETCH”的支持正在增加。它提供了一个加载脚本而不解析和执行脚本的途径。浏览器提供商需要确保LINK标签有一个load事件,这样开发者才能够知道文件是否已经下载完毕。进而存放在浏览器缓存中的文件可以在真正需要时通过动态添加DOM 节点的异步方式或其他技术添加到页面中。

我们即将可以进入到脚本加载优化的下一阶段。而在万事俱备之前,先驱开发者们将如往常一样做他们应做的——继续努力,紧随最新的最佳实践。若想完全的掌控脚本加载时间点以及解析、执行的时间点,我推荐你阅读来自Gmail MobileStoyan的相关文章。

鉴于过去几年的判断展情况,我打赌,脚本加载有更多的提升空间等着我们。

发表在 未分类 | 标签为 , | 一条评论

HTML5 for Windows XP

html5-for-xp最近Mozilla的技术布道师Christian Heilmann建了一个网站,号召Windows XP用户抛弃IE,转投其他更好的浏览器,以享受HTML5所带来的良好Web体验。本人为之提供了简体中文版的翻译,欢迎大家阅读传播,批评指正,同时一起来推广HTML5。

发表在 未分类 | 标签为 | 留下评论

从父窗口调用子窗口的方法

这是一个BOM问题,我这里指的子窗口(child window)是指从一个窗口通过window.open或iframe技术所创建的window对象,当然,父窗口(parent window)就是创建子窗口的原始窗口。网上总是有许多介绍从子窗口调用父窗口的方法的文章,但没有讲如何从父窗口调用子窗口方法,这里晒一晒最近做项目摸索出的小窍门,看代码吧:

var childWin, interval,
     callFunc = function(){
           if(childWin.childFunc){   //childFunc表示试图调用的存在于子窗口的函数
                childWin.childFunc();
                window.clearInterval(interval);
           }
       };
interval =  window.setInterval(callFunc,300);
childWin = window.open('some-url', 'some-windowname');

由于window.open是异步执行的,子页面的加载需要一定时间,也就是说childFunc并不是马上就会出现,所以做了一个300ms的轮循来检查childFunc的可用性。当然,由于安全因素,此方法只适用于两个页面主域相同的情况。

发表在 老技术 | 标签为 , | 3 条评论

My first Safari 5 bug

听说Safari5比Firefox快1倍,甚至比Chrome还要快一点(3%)。我也迫不及待地将Safari更新至最新版本(Safari for Windows)。最近试用时发现了一个bug,现发布在此,看看大家有无此现象。

该bug的现象是:通过与页面交互而产生的阻塞(block)窗口并不能阻止用户继续对页面内容进行操作,例如:在我的测试页面中,点击彩色的区块可以产生一个alert对话框,按照正确的处理方式,在关闭此对话框之前我们将不能对页面进行操作,然而事实不是这样,你仍然可以通过鼠标激发每个彩色区块的hover状态、可以继续点击彩色区块弹出alert对话框、可以点击页面上的链接(Link 1),你甚至可以穿过对话框点击一个看不见的链接(Link 2),不信你试试,很疯狂吧?目前我知道的能回到正常轨道的方法是:用鼠标点击一下页面上还没有绑定事件的区域。

我刚刚提到,这个bug的出现需要与页面交互,例如在页面加载完成后自动弹出的alert并没有问题;同时,这个bug同样存在于promt和confirm函数。

虽然alert、promt、confirm如今已经用得很少了,但当这个bug出现时,还是会造成更差的使用体验和安全隐患的。

发表在 老技术 | 4 条评论

无厘撞头

昨晚梦见自己埋头狂奔,结果头直接狠狠撞在墙上,疼痛感“由外及里,再由里向外”,相当逼真(小时候荡秋千,不幸绳索断了,头着地,也是这种感觉),甚至在我醒来后一段时间还在担心自己会不会被撞出脑震荡、撞成脑残,不过根据今天一天的表现,大脑工作还算正常。

基于以上这个扯淡的梦,我在想一件事:疼痛并不一定需要外界的刺激,比如想重温撞墙的感觉并不一定要去撞墙一样,疼痛过程完全可以被大脑记录下来,然后在特定情况下播放即可重演疼痛感觉,或许其他知觉也同样适用于这种猜想。这有点像计算机的宏,可录制,可重复播放。

看起来不错,我突然有种科学家的感觉,不过以上猜想基于本人是梦见撞墙的事实,那万一我是真的撞墙了呢?比如梦游的时候。你别较真,都说了是个扯淡的梦……

发表在 老乱想 | 一条评论

IE6 resize event bug 解决方案

最近写组件时碰到一个问题:对window对象监听resize事件,当浏览器窗口大小发生改变时,在IE6和IE7下监听函数会被执行多次,这与我们期望的执行一次可不吻合。更可怕的是,在某些DTD下,DOM中某个元素区块的大小发生变化时也会激发window的resize监听函数,且在IE6和IE7下看我的例子。在这个例子里,当点击”Zoom In”或”Zoom Out”改变红框大小时,激发了window的resize事件,不过元素的位置移动不会导致这个bug,不信点击”Move”按钮移动绿框试试,以上这些奇怪的效果IE7同样存在。至于浏览器窗口大小发生改变时监听函数反复执行的错误就与DTD没有关系了。为了防止函数多次执行,每次window的resize事件发生时,我们可以判断一下浏览器窗口大小是否改变了,只有真的改变时我们才执行监听函数,Javascript代码大概会是这样:

...
var width = document.documentElement.clientWidth,
     height = document.documentElement.clientHeight;
window.onresize = function(){
	if(width != document.documentElement.clientWidth ||
           height = document.documentElement.clientHeight){
		resizeFunc();
	}
	width = document.documentElement.clientWidth;
        height = document.documentElement.clientHeight;
}
...

为什么不加一个浏览器检测呢,那样在非IE浏览器下就可以用常规写法了。可是据我测试,风头不小的Chrome也会犯这个错误。所以,还是以防万一吧。

发表在 老技术 | 标签为 , , | 2 条评论

现实照进梦乡

湖滨猜想

杭州的公交编号用K开头,估计就是传说中的快速公交,但可不快;红绿灯不喜欢提示你时间,就是那种倒数装置,我很奇怪,杭州人民怎么都能感觉到呢?杭州有一个公交站叫湖滨,我和Shirley走在湖滨的街头有莫名的恐惧感,深怕遇见胡斌,别说红灯亮时不敢移动半步,绿灯亮了也得“左顾右盼”,过马路俨然成为我们的一大难题;然而事实并非想象的那样可怕,因为街上勇闯红灯横穿马路的大有人在,并且都是些中老年选手,他们从不“左看看,右看看”,却能轻松到达左岸。我是一个爱观察的人,这一颠覆老师教导的现象立刻引起了我的注意:能否顺利通过马路与遵守红绿灯之间并不存在直接关系,根据统计分析,与试图通过马路的试验者的年龄有一定的关系。这姑且命名为湖滨猜想。

“鸡鸭鱼肉”歌

窗外的“鸡鸭鱼肉”歌将我从梦乡照进现实,“鸡鸭鱼肉”歌?就是周立波扮演的周立波在《一周·立波秀》中演唱的那段原创昆曲,当然也是我的第一首听清了歌词大意的昆曲。根据考证,由于小区里某人不幸离世,家人请来了“国家一级演员、国家二级演员”(报幕员原话),声音之大让我怀疑他们家是不是动用了小区里的广播系统。okay,本人头一次体会这样的东部风俗,除了不能与他家人一起感到悲伤以外,其他一切照旧。

明天要去公司报到了,天还是在下雨。

发表在 老日记 | 4 条评论

友情链接一二

今天一起将老友棉胎和Blues的blog链接更新了,棉胎的blog其实前几天就上线了,重新改名叫“棉胎急呼SOS”,看得出这家伙必要看不住自己的嘴巴了;Blues的”Zen Log”名字很酷,Zen!

棉胎急呼SOS:http://www.bcxw.org

Zen Log:http://zenzlog.com

很高兴周围又有朋友加入了独立博客的生活。。。

发表在 老日记 | 3 条评论

用YQL帮助显示Twitter消息

我们经常看到一些Blog或者内容发布站点会将自己的Twitter消息显示在网站的某个位置(比如边栏),这些功能大多都是由后台程序(如PHP)来完成,若没有个人可管理的服务器或者不支持运行后台程序将无法使用;使用JavaScript版的则需要浏览器端能正常访问Twitter数据源,但由于某些网络原因也无法实现。

今天自己动手写了一个基于YQL和JavaScript的简单脚本,可以帮助人们实现显示个人Twitter消息的需求,本页面边栏的“Tweets”就是使用它来实现的。所以只要你的站点可以运行JavaScript,并且能访问YAHOO! API ;那么就放心地将下面的代码嵌入到你想在自己站点上显示Twitter 的地方吧,如下:

<script type=”text/javascript” src=”http://swayweb.com/labs/twitter_timeline.js”>id:swaydeng,limit:5,style:false</script>

其中’swaydeng’替换成你的TwitterId,limit代表你打算显示twitter的条数(小于等于20),style表示嵌入的twitter消息框是否需要加入CSS样式,目前是false。

就是这么简单,欢迎使用。

发表在 老技术 | 标签为 , | 一条评论

1px Dotted Border Bug In Chrome3.0

这是一个关于浏览器对CSS属性border的解析问题。在IE6下,当把边框(border-width)设置为1像素并且边框样式(border-style)设置为dotted时,IE6通常会渲染成虚线(dashed)而不是我们想要的点线(dotted),这就是古老的1px Dotted Border Bug In Internet Explorer 6,好消息是在IE6以后的版本里修复了这个bug。

然而最近做测试时发现,以上提到的IE6的bug很不幸地转移到了Chrome上,具体现象是:在Chrome3.0下,当把边框(border-width)设置为1像素并且边框样式(border-style)设置为dotted时,无论长短,线段中部都会有一段实线,只有当border-width大于1px时才正常,很奇怪;补充一点,同样使用WebKit引擎的Safari3.2并不存在这个错误。

本人的测试环境:操作系统是WinXp Pro,屏幕分辨率是1280×800,浏览器是Chrome3.0.195.33。我在想,这难道是Chrome的一个渲染bug?模仿IE,先称此bug为1px Dotted Border Bug In Chrome3。

测试页面(Test Page)

发表在 老技术 | 标签为 , | 留下评论