Hello World
Spiga

挣脱浏览器的束缚(4) - 王道!动态添加script元素

2007-01-25 01:19 by 老赵, 8388 visits

我们已经知道,脚本文件的并行下载能够提高页面的加载速度。但是目前还有一个急需解决的问题,那就是对于FireFox浏览器的优化。在我们之前使用的优化方法,无论是简单实用的document.write还是食之无味的defer属性,FireFox浏览器都对此置若罔闻。不过FireFox也不是绝对地“冥顽不灵”,开发人员还是有方法对它进行优化的。

这个方法就是动态添加script元素。

动态添加script元素

不知道“动态添加script元素”这个说法是否正确,我在这里的意思是使用JavaScript编程,向<head />里添加script元素。下面的代码动态添加了5个script元素:

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server" id="head">
    <title>Untitled Page</title>
    <script type="text/javascript" language="javascript">
        for (var i = 0; i < 5; i ++)
        {
            var script = document.createElement("script");
            script.type = "text/javascript";
            script.src = "Script.ashx?a=" + i;
            document.getElementById('head').appendChild(script);
        }
    </script>
    
</head>
<body>
    ...
</body>
</html>

 

请注意,由于在JavaScript代码执行时页面还没有加载完毕,因此还不能使用document.getElementsByTagName方法来获得head元素,我们只能为head元素添加一个id,并使用document.getElementById方法来获得它。打开这张页面,就会发现,无论是IE(图9)还是FireFox(图10)的元素加载都会发现优化的效果:


图9:IE中动态加载script元素效果


图10:FireFox中动态加载script元素效果

我们姑且不关心为什么FireFox中每个脚本文件会使用2.5秒进行加载,但是并行加载的效果切切实实的出现了!加上多域名,效果更明显。

细心的朋友不知道回想起什么了吗?没错,当年在ASP.NET AJAX某个版本中(我记得是Beta 1,有些模糊了)加载自定义脚本时使用了Sys.Application.queueScriptReference方法,它能够让脚本文件并行加载。但是由于接下来会谈到的几个问题,最终还是选择了传统的加载方式。不过ASP.NET AJAX还是细心地考虑到脚本加载的影响,ScriptManager和ScriptReference已经提供了LoadScriptsBeforeUI属性,我们现在就能够控制script元素是出现在UI之前还是之后了,我们可以影响性能但是无需“急用”的脚本放在所有UI的最后进行加载,以降低它对于性能的影响(这个是在刚发布的ASP.NET AJAX正式版中新增的功能,我在阅读代码时无意发现)。

再说句题外话,虽然这个脚本加载方法已经被取消了,但是功能依旧存在,因为UpdatePanel在Partial Rendering之后只能选择动态加载脚本文件。我们也能够自己使用这样的加载方式,然而这就超出了今天这篇文章讨论的范围。不过既然ASP.NET AJAX正式版已经发布了,我也能够放心的继续《深入Atlas系列》了。:)

 

动态添加script元素的缺陷

世界上很少有完美的事物。动态添加的script元素能够使IE和FireFox里都得到优化,它应该也会有些麻烦,否则为什么这个方法没有被推广呢?

而且事实上,动态添加script元素的做法是“优化难度”最高的方法。我现在就来一一列举这些“缺陷”:

1、无法阻碍页面加载

其实这个问题和在IE中使用defer属性遇到的问题相同。如果您需要在页面中直接使用脚本文件里定义的函数,就不能轻易使用这个做法。即使它的确能够优化页面的加载速度。

2、影响window.onload事件的触发

如果对于window.onload事件的处罚有所影响,但是这种影响能够在不同浏览器中得到统一倒也罢了,还相对容易处理一些。现在的问题就在于,在IE中,window.onload事件会在页面其它元素被加载完毕之后立即触发,而FireFox里的window.onload事件会等待动态添加的那些脚本文件也被加载完毕后才触发。虽然我们开发人员是伟大的,可是要兼容这两种情况依旧不是一件易如反掌的事情。

3、动态加载脚本的执行顺序

这一点才是最致命的。

虽然我们动态加载的script元素是有严格顺序的,但是浏览器可不一定这样认为。在FireFox中,脚本文件会按照它动态加载的script元素的顺序执行,而IE会根据脚本文件下载完毕的顺序执行。

那还得了?

 

那么为何称之为王道?

既然麻烦这么多,为什么还称之为“王道”?其实我们只要合理的使用这个方法,就能够大大提高页面的Perceived Performance。

可能在这里我需要重新定义一下“Perceived Performance”的概念。它的意思是“用户感受到的性能”。我们打开一个页面,例如Windows Live个人主页,会发现页面的框架都被加载了,但是每一个框架都是Loading状态。然后每一个模块陆陆续续地加载成功。

我们来想象一下这个场景。一个页面的所有内容(包括模块),需要20秒钟才能加载完毕。但是它用了10秒钟就显示出了模块的框架,在接下来10秒钟内每个模块慢慢的出现。还有一种情况,就是等待整整20秒才能看到页面。从用户角度来看,哪个性能比较高呢?

这个就是Perceived Performance的经典应用。从所谓的Web 2.0开始,Perceived Peformance的重要性可以说被提高到了一个前所未有的高度。

那么我们现在就用语言来简单描述一下应该如何实现这样的效果:

  1. 首先,在页面上用传统方式(最好使用document.write)加载所需要的基础脚本以及所有的HTML,这时候所有的模块处于Loading状态。
  2. 在window.onload事件被触发后,动态加载每个模块所需的脚本。我们只需要在IE浏览器中响应script元素的onload事件或者在其它浏览器中响应script元素的onreadystatechange事件,就可以捕捉脚本文件的加载情况。
  3. 在上述事件的handler中,如果script元素的readyState为"complete"或"loaded"(script元素的readyState使用字符串表示),那么判断某个模块需要的脚本有没有加载完毕,如果完毕了,则显示那个模块的具体内容。

大体方式就是这样,逻辑非常简单,不过在编码上可能就会遇到一些问题。不过对于使用ASP.NET AJAX的开发人员可能就略有福气些了,因为ASP.NET AJAX内置就有动态添加脚本元素的机制,已经实现了很好的跨浏览器特性。它们能够稍稍便于我们的开发,有机会我将详细的介绍它们,并且和大家一起来设计和实现一个好用的脚本库。

 

我对于加载脚本文件的优化心得就只有这些了,不过我们还可以在其他方面进行优化。例如,AJAX应用里最常见的XMLHttpRequest对象,我们也可以有技巧地使用它。不过这些内容,就要等下次再和大家分享了。:)

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

58 条回复

  1. 有点麻烦[未注册用户]
    *.*.*.*
    链接

    有点麻烦[未注册用户] 2007-01-25 08:25:00

    沙发???
    好文章啊...期待下一篇文章...

  2. 有点麻烦[未注册用户]
    *.*.*.*
    链接

    有点麻烦[未注册用户] 2007-01-25 08:27:00

    顺便把板凳给拿走了哈。。。
    老赵,ajax系列是不是快动工了?期待哈。。。

  3. 臭石头
    *.*.*.*
    链接

    臭石头 2007-01-25 08:33:00

    太强了,不仅仅在于脚本优化,我所得到的更多的是:知道了一些以前所不知道的东西,比如“由于在JavaScript代码执行时页面还没有加载完毕,因此还不能使用document.getElementsByTagName方法来获得head元素”

  4. 有点麻烦[未注册用户]
    *.*.*.*
    链接

    有点麻烦[未注册用户] 2007-01-25 08:42:00

    @臭石头
    同感,但是我更想知道老赵是怎么知道的...

  5. ningsia[未注册用户]
    *.*.*.*
    链接

    ningsia[未注册用户] 2007-01-25 08:44:00

    我们只能为head元素添加一个id,并使用document.getElementById方法来获得它。

    -----------
    这种方法其实也不太合适,也不符合XHTML标准的,因为在XHTML中,head标签是没有id属性,原因是一个页面有且仅有一个head标签,不存在要表示id的问题。

  6. 老赵
    admin
    链接

    老赵 2007-01-25 08:46:00

    @有点麻烦
    谢谢。:)

  7. 老赵
    admin
    链接

    老赵 2007-01-25 08:49:00

    @有点麻烦
    这要看我的时间了……:(

  8. 老赵
    admin
    链接

    老赵 2007-01-25 08:50:00

    @有点麻烦
    用了就知道了。:)

  9. 老赵
    admin
    链接

    老赵 2007-01-25 08:50:00

    @臭石头
    呵呵,可是知道这点没有大用阿……

  10. 老赵
    admin
    链接

    老赵 2007-01-25 08:52:00

    @ningsia
    这点我没有注意,看来我需要重新读一下标准了,谢谢您的提醒。
    因为在这里,无法使用document.getElementsByName来取得head,所以只能出此下策了。不过只要按照我文章里提到的做法,在window.onload事件触发之后再添加script元素,就可以使用document.getElementsByName了。:)

  11. 有点麻烦[未注册用户]
    *.*.*.*
    链接

    有点麻烦[未注册用户] 2007-01-25 08:56:00

    @Jeffrey Zhao
    时间总是不够用的

  12. 老阿伯[未注册用户]
    *.*.*.*
    链接

    老阿伯[未注册用户] 2007-01-25 08:57:00

    好东西,的确速度提高了不少~

  13. webabcd
    *.*.*.*
    链接

    webabcd 2007-01-25 09:05:00

    收藏阿

  14. Go_Rush
    *.*.*.*
    链接

    Go_Rush 2007-01-25 09:23:00

    对于优化,动态加载Script确实是很可行的办法。

    文中提到的缺陷,其实也不是什么问题,因为最重要的,必须先加载的页面都
    可以用传统的方法加载。

    不过不知道老赵有没有遇到过这样的问题。

    var js=document.createElement("script");
    js.src="..."

    这种加载方式并不稳定,偶尔会出现加载失败。

    这种动态加载script的方式我曾应用在一个项目中。动态加载3个js
    但是并不是每次都能成功加载的。 总会有一两次出现加载失败(1%的机率)

    这种不稳定性令我恨饶火。

  15. Go_Rush
    *.*.*.*
    链接

    Go_Rush 2007-01-25 09:28:00

    ::而FireFox里的window.onload事件会等待动态添加的那些脚本文件也被加载完毕后才触发

    如果要用动态加载,肯定是在 onload事件里面加载,效果最优。

    < script src="必须页面加载时加载的脚本.js">< /script>
    < script>
    window.onload=function(){
    var js=document.createElement("SCRIPT")
    js.src="可以延迟加载而且加载次序也无关紧要的脚本.js"
    }
    < /script>

  16. 老赵
    admin
    链接

    老赵 2007-01-25 09:34:00

    @Go_Rush
    为什么会有这个问题呢?我好像没有遇见过……
    其实在Windows Live系列产品里,您打开HTML源代码之后会发现<web:binding>这类的XML格式,它引用的js都是被动态创建的。似乎没有什么问题。

  17. 老赵
    admin
    链接

    老赵 2007-01-25 09:35:00

    @有点麻烦
    那么就有点麻烦了,呵呵。:)

  18. 老赵
    admin
    链接

    老赵 2007-01-25 09:35:00

    @Go_Rush
    您说的没错,我一开始这么做只是为了说明一些问题。
    其实动态加载脚本的这种做法已经可以说是一种Pattern了。:)

  19. yunhuasheng
    *.*.*.*
    链接

    yunhuasheng 2007-01-25 09:35:00

    有新收获,!

  20. 虫虫[未注册用户]
    *.*.*.*
    链接

    虫虫[未注册用户] 2007-01-25 09:40:00

    好文章。
    不过可能搜索引擎不买帐。

  21. 老赵
    admin
    链接

    老赵 2007-01-25 09:43:00

    @虫虫
    脚本文件的加载方式这个和搜索引擎有什么关系呢?

  22. 在北京的湖南人
    *.*.*.*
    链接

    在北京的湖南人 2007-01-25 10:26:00

    我刚才提交评论的时候,dudu为防止重复多次点击提交,所以在提交前禁止了button,但是却返回给我一大串英文提示,大概是说通信失败,但是button却一直是disable掉了,所以只有刷新才能再次提交。。。这算不算一个博客园的Bug 啊?呵呵。在返回非200状态值得时候把button状态还原

  23. 老赵
    admin
    链接

    老赵 2007-01-25 10:30:00

    @在北京的湖南人
    这个问题存在很久了。:)

  24. 在北京的湖南人
    *.*.*.*
    链接

    在北京的湖南人 2007-01-25 10:32:00

    。。。。。看来是因为我的留言少,所以才发现,呵呵

  25. 老赵
    admin
    链接

    老赵 2007-01-25 10:35:00

    @在北京的湖南人
    呵呵,可以向dudu提个意见。
    不过有的时候也是已经提交成功了,但是response失败。

  26. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-01-25 12:05:00

    Opera和Safari有没有测试过?

  27. 老赵
    admin
    链接

    老赵 2007-01-25 12:11:00

    @Cat Chen
    没有测试条件……

  28. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-01-25 14:18:00

    @Jeffrey Zhao
    Opera9到官方网站就可以下载吧,Safari有点麻烦,不过它是基于Webkit的,Webkit是一个开源的Web浏览器项目,不知道可否直接编译一个简单的版本用于测试。

  29. 老赵
    admin
    链接

    老赵 2007-01-25 14:36:00

    @Cat Chen
    我的意思不是说没有使用的条件,而是说没有测试的条件。
    比如FireFox下要FireBug,IE下要HttpWatch……

  30. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-01-25 14:55:00

    @Jeffrey Zhao
    head确实不应该有id的。

    在ASP.NET 1.x的时候,可以通过<head id="head" runat="server" />来创建一个HtmlGenericControl,方便控制head的内容,就如2.0的HtmlHead一样。不过这样输出的HTML就会有id,然后就无法通过XHTML验证。

  31. 老赵
    admin
    链接

    老赵 2007-01-25 14:59:00

    @Cat Chen
    还好,一般不用id。

  32. aw[未注册用户]
    *.*.*.*
    链接

    aw[未注册用户] 2007-01-25 17:38:00

    我很好奇, 为什么要在head里加入
    runat = server,干吗用的?

  33. 老赵
    admin
    链接

    老赵 2007-01-25 18:03:00

    @aw
    为了能够在服务器端操作Head,比如设置title。

  34. 有点麻烦[未注册用户]
    *.*.*.*
    链接

    有点麻烦[未注册用户] 2007-01-25 22:24:00

    想问大家个问题
    在用Forms Authentication时如果在web.config中加入authentication验证的话在脚本调用方法时会发生缺少对象。
    不知道大伙有没有人碰到这个问题的
    附上官方doc地址:http://ajax.asp.net/docs/tutorials/UsingFormsAuthenticationTutorial.aspx

  35. 老赵
    admin
    链接

    老赵 2007-01-25 23:20:00

    缺少什么对象啊?

  36. stonezhu
    *.*.*.*
    链接

    stonezhu 2007-01-26 00:26:00

    真是受教了,谢谢"老赵"

  37. stonezhu
    *.*.*.*
    链接

    stonezhu 2007-01-26 00:37:00

    再问一个和主题不相干的问题,希望老赵能帮帮忙
    我最近在做AJAX应用时,遇到一个图片高宽处理的问题,就是怎么样才能立刻获得我一个图片的WIDTH,HEIGHT.然后再把WIDTH,HEGITH传给一个方法进行计算,生成的一个缩略图的WIDTH,HEIGHT.
    我现在是用这样的一个方法:
    var tmpImage = document.createElement("IMG");
    tmpImage.src="http://image.***.com/ImageServer/***/my.jpg";
    var tmpArr = GetImgSize(tmpImage.Width,tmpImage.Height);
    这样得到一个Width,Height
    但是这样的方法存在一个严重的问题,这样很可能tmpImage这个时候还没有加载到Client端,这样导致我传进"GetImgSize()"方法的参数没有值.
    我现在还不知道该怎么解决这样的问题,希望能指点迷津啦,
    Thank you

  38. 老赵
    admin
    链接

    老赵 2007-01-26 00:41:00

    @stonezhu
    您必须在它的onload事件中才能得到width和height
    var img = document.createElement('img');
    img.onload = function(){ alert(this.width); alert(this.height); }
    img.src = 'my.jpg';

  39. stonezhu
    *.*.*.*
    链接

    stonezhu 2007-01-26 00:45:00

    @Jeffrey Zhao
    那你的意思是我只有这它的onload事件中得到它的width,height然后再进行相关计算吗?
    谢谢,你睡的还挺晚的,这么晚还回复我,
    TNANK YOU VERY MUCH:)

  40. 老赵
    admin
    链接

    老赵 2007-01-26 00:50:00

    @stonezhu
    因为只有在onload事件被触发才表示图片被加载完了。
    // 我睡得很晚,所以没关系的。:)

  41. stonezhu
    *.*.*.*
    链接

    stonezhu 2007-01-26 00:54:00

    @Jeffrey Zhao
    :)明白了,谢谢.搞程序的多数晚睡...
    最近一直在做AJAX应用,用的Prototype.
    以前对这方面不太了解.
    那我的相关方法都要在onload=function(){...};这个里面进行喽.

  42. stonezhu
    *.*.*.*
    链接

    stonezhu 2007-01-26 00:59:00

    @Jeffrey Zhao
    谢谢啦,我要休息了,你年纪也很轻,早点休息喽,苦命本钱嘛...
    Goog night

  43. 老赵
    admin
    链接

    老赵 2007-01-26 01:07:00

    @stonezhu
    没错,必须这么做的。
    我晚睡是因为喜欢阿,搞技术很爽的。:)

  44. 老赵
    admin
    链接

    老赵 2007-01-26 15:39:00

    safari下没有load事件或者readystatechange事件,因此需要调用一个notifyScriptLoaded()方法。

  45. 萝卜[未注册用户]
    *.*.*.*
    链接

    萝卜[未注册用户] 2007-01-29 11:27:00

    动态加载脚本早前我就在使用这种方法了,类似于利用一个公共模块管理的方法,通过自定义的语句引入需要加载的包。
    但是带来的问题就是,如果大量采用这种方法,因为加载的过程是异步的,所以在使用加载的包中的内容的时候,总是需要先判断其加载状态,这点比较麻烦而且感觉不太好控制。

  46. 老赵
    admin
    链接

    老赵 2007-01-29 11:58:00

    @萝卜
    为什么说麻烦呢?有onload和onreadystatechange事件,都是比较容易控制的。:)

  47. 萝卜[未注册用户]
    *.*.*.*
    链接

    萝卜[未注册用户] 2007-01-29 17:49:00

    麻烦的地方在于:要做到按需求加载模块.并不完全是页面初始化的时候.也可能发生在用户的操作过程中。
    我设定一个加载的触发条件,用户在操作到某一步的时候执行这个加载,而加载过程是异步的,需要的时间未知,我需要在加载完成后马上完成一系列操作,而剩下的一系列操作又受这一部分操作的影响。这样,在编写代码的时候,只要是涉及到动态加载的包中的内容的部分,都需要预先进行状态判断,而后用类似timeout或者interval的方式来处理以保证后续操作能够自动完成.当代码量大了之后就会变的异常烦琐而且感觉上不好控制。
    不知道我表述是否清楚,又或者我的处理方法不够精明。我只是觉得一开始就把所需要的包全部加载并不是很好的办法,最好是在用户实际的操作过程中按需要来加载.

  48. 老赵
    admin
    链接

    老赵 2007-01-29 18:09:00

    @萝卜
    如果可以的话全部加载一般也没有什么问题。除非是很明显的“按需”加载需求。
    我还是没有理解setTimeout或setInterval的作用是什么,不是加载完成有onload或者onreadystatechange回调函数吗?即使错误了也有onerror回调函数的说。
    // 支持safari回麻烦些,因为没有onload或onreadystatechange。

  49. 萝卜[未注册用户]
    *.*.*.*
    链接

    萝卜[未注册用户] 2007-01-29 21:31:00

    setTimeout,setInterval起到跟onload及onreadystatechange同样的作用,更兼容的方法吧,就是判断加载是否完成,如果完成则执行语句,否则则等待一定时间间隔重新判断。

  50. horsefaced[未注册用户]
    *.*.*.*
    链接

    horsefaced[未注册用户] 2007-08-31 14:15:00

    document.write('<script type="text/javascript" language="javascript" src="' + src + '"></script>');

    实际中,这么一句就可以了,不用给head加id, 也不用写什么,嗯,google就是这么干的。当时文章中所说的加载顺序什么的问题也还是继续存在的。

  51. ╃小〥斌╄
    *.*.*.*
    链接

    ╃小〥斌╄ 2007-08-31 14:27:00

    可以把src 指向到一个action , action里是动态生成的该页面所需要的js。
    不光js ,css 也同样需要优化的。

  52. 老赵
    admin
    链接

    老赵 2007-08-31 14:45:00

    @horsefaced
    对FireFox没有效果,我这篇文章就是为了解决FireFox的问题的。

  53. 老赵
    admin
    链接

    老赵 2007-08-31 14:46:00

    @╃小〥斌╄
    的确可以这样,不过这个和优化有什么关系呢?

  54. ╃小〥斌╄
    *.*.*.*
    链接

    ╃小〥斌╄ 2007-08-31 15:11:00

    优化不光是速度 性能 还有安全吧。

    这样做可以防止用纯src 来下载js,css 。 屏蔽非本站发出的请求。

  55. 老赵
    admin
    链接

    老赵 2007-08-31 15:27:00

    @╃小〥斌╄
    呵呵,那么就是吧。:)

  56. horsefaced[未注册用户]
    *.*.*.*
    链接

    horsefaced[未注册用户] 2007-08-31 16:40:00

    不好意思,我说的那个方法是可以的,我刚刚在FIREFOX上试过了,代码如下:
    var included = new Array();

    /** 引用其他脚本文件 */
    function include(src) {
    if (!included[src]) {
    document.write('<script type="text/javascript" language="javascript" src="' + src + '"></script>');
    included[src] = src;
    }
    }

    include("utils/TestNavigator.js");
    include('map/mapconst.js');
    include('map/map.js');
    include('map/action/DragAction.js');

  57. 老赵
    admin
    链接

    老赵 2007-08-31 21:36:00

    @horsefaced
    呵呵,其实我这个系列的(1)就讲了这个内容,不过我当时试验的结果是不成功的阿。

  58. Liner_z
    219.133.0.*
    链接

    Liner_z 2013-06-25 11:47:19

    汗,以为上博客园了。。。 选择性加载js文件是个不错的功能,用在iframe里也不错,可以在不修改iframe的情况下选择性加入js。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我