Spiga

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

2007-01-25 01:19 by Jeffrey Zhao, 4819 visits, 网摘, 编辑

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

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

动态添加script元素

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

动态添加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对象,我们也可以有技巧地使用它。不过这些内容,就要等下次再和大家分享了。:)

Add your comment

59 条回复

  1. #1楼 有点麻烦[未注册用户]2007-01-25 08:25
    沙发???
    好文章啊...期待下一篇文章...
      回复  引用    
  2. #2楼 有点麻烦[未注册用户]2007-01-25 08:27
    顺便把板凳给拿走了哈。。。
    老赵,ajax系列是不是快动工了?期待哈。。。
      回复  引用    
  3. #3楼 臭石头      2007-01-25 08:33
    太强了,不仅仅在于脚本优化,我所得到的更多的是:知道了一些以前所不知道的东西,比如“由于在JavaScript代码执行时页面还没有加载完毕,因此还不能使用document.getElementsByTagName方法来获得head元素”
      回复  引用  查看    
  4. #4楼 有点麻烦[未注册用户]2007-01-25 08:42
    @臭石头
    同感,但是我更想知道老赵是怎么知道的...
      回复  引用    
  5. #5楼 ningsia[未注册用户]2007-01-25 08:44
    我们只能为head元素添加一个id,并使用document.getElementById方法来获得它。

    -----------
    这种方法其实也不太合适,也不符合XHTML标准的,因为在XHTML中,head标签是没有id属性,原因是一个页面有且仅有一个head标签,不存在要表示id的问题。
      回复  引用    
  6. #6楼[楼主] Jeffrey Zhao      2007-01-25 08:46
    @有点麻烦
    谢谢。:)
      回复  引用  查看    
  7. #7楼[楼主] Jeffrey Zhao      2007-01-25 08:49
    @有点麻烦
    这要看我的时间了……:(
      回复  引用  查看    
  8. #8楼[楼主] Jeffrey Zhao      2007-01-25 08:50
    @臭石头
    呵呵,可是知道这点没有大用阿……
      回复  引用  查看    
  9. #9楼[楼主] Jeffrey Zhao      2007-01-25 08:50
    @有点麻烦
    用了就知道了。:)
      回复  引用  查看    
  10. #10楼[楼主] Jeffrey Zhao      2007-01-25 08:52
    @ningsia
    这点我没有注意,看来我需要重新读一下标准了,谢谢您的提醒。
    因为在这里,无法使用document.getElementsByName来取得head,所以只能出此下策了。不过只要按照我文章里提到的做法,在window.onload事件触发之后再添加script元素,就可以使用document.getElementsByName了。:)
      回复  引用  查看    
  11. #11楼 有点麻烦[未注册用户]2007-01-25 08:56
    @Jeffrey Zhao
    时间总是不够用的
      回复  引用    
  12. #12楼 老阿伯[未注册用户]2007-01-25 08:57
    好东西,的确速度提高了不少~
      回复  引用    
  13. #13楼 webabcd      2007-01-25 09:05
    收藏阿
      回复  引用  查看    
  14. #14楼 Go_Rush      2007-01-25 09:23
    对于优化,动态加载Script确实是很可行的办法。

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

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

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

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

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

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

      回复  引用  查看    
  15. #15楼 Go_Rush      2007-01-25 09:28
    ::而FireFox里的window.onload事件会等待动态添加的那些脚本文件也被加载完毕后才触发

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

    < script src="必须页面加载时加载的脚本.js">< /script>
    < script>
    window.onload=function(){
    var js=document.createElement("SCRIPT")
    js.src="可以延迟加载而且加载次序也无关紧要的脚本.js"
    }
    < /script>
      回复  引用  查看    
  16. #16楼[楼主] Jeffrey Zhao      2007-01-25 09:34
    @Go_Rush
    为什么会有这个问题呢?我好像没有遇见过……
    其实在Windows Live系列产品里,您打开HTML源代码之后会发现<web:binding>这类的XML格式,它引用的js都是被动态创建的。似乎没有什么问题。
      回复  引用  查看    
  17. #17楼[楼主] Jeffrey Zhao      2007-01-25 09:35
    @Go_Rush
    您说的没错,我一开始这么做只是为了说明一些问题。
    其实动态加载脚本的这种做法已经可以说是一种Pattern了。:)
      回复  引用  查看    
  18. #18楼 yunhuasheng      2007-01-25 09:35
    有新收获,!
      回复  引用  查看    
  19. #19楼[楼主] Jeffrey Zhao      2007-01-25 09:35
    @有点麻烦
    那么就有点麻烦了,呵呵。:)
      回复  引用  查看    
  20. #20楼 虫虫[未注册用户]2007-01-25 09:40
    好文章。
    不过可能搜索引擎不买帐。
      回复  引用    
  21. #21楼[楼主] Jeffrey Zhao      2007-01-25 09:43
    @虫虫
    脚本文件的加载方式这个和搜索引擎有什么关系呢?
      回复  引用  查看    
  22. #22楼 在北京的湖南人      2007-01-25 10:26
    我刚才提交评论的时候,dudu为防止重复多次点击提交,所以在提交前禁止了button,但是却返回给我一大串英文提示,大概是说通信失败,但是button却一直是disable掉了,所以只有刷新才能再次提交。。。这算不算一个博客园的Bug 啊?呵呵。在返回非200状态值得时候把button状态还原
      回复  引用  查看    
  23. #23楼[楼主] Jeffrey Zhao      2007-01-25 10:30
    @在北京的湖南人
    这个问题存在很久了。:)
      回复  引用  查看    
  24. #24楼 在北京的湖南人      2007-01-25 10:32
    。。。。。看来是因为我的留言少,所以才发现,呵呵
      回复  引用  查看    
  25. #25楼[楼主] Jeffrey Zhao      2007-01-25 10:35
    @在北京的湖南人
    呵呵,可以向dudu提个意见。
    不过有的时候也是已经提交成功了,但是response失败。
      回复  引用  查看    
  26. #26楼 Cat Chen      2007-01-25 12:05
    Opera和Safari有没有测试过?
      回复  引用  查看    
  27. #27楼[楼主] Jeffrey Zhao      2007-01-25 12:11
    @Cat Chen
    没有测试条件……
      回复  引用  查看    
  28. #28楼 Cat Chen      2007-01-25 14:18
    @Jeffrey Zhao
    Opera9到官方网站就可以下载吧,Safari有点麻烦,不过它是基于Webkit的,Webkit是一个开源的Web浏览器项目,不知道可否直接编译一个简单的版本用于测试。
      回复  引用  查看    
  29. #29楼[楼主] Jeffrey Zhao      2007-01-25 14:36
    @Cat Chen
    我的意思不是说没有使用的条件,而是说没有测试的条件。
    比如FireFox下要FireBug,IE下要HttpWatch……
      回复  引用  查看    
  30. #30楼 Cat Chen      2007-01-25 14:55
    @Jeffrey Zhao
    head确实不应该有id的。

    在ASP.NET 1.x的时候,可以通过<head id="head" runat="server" />来创建一个HtmlGenericControl,方便控制head的内容,就如2.0的HtmlHead一样。不过这样输出的HTML就会有id,然后就无法通过XHTML验证。
      回复  引用  查看    
  31. #31楼[楼主] Jeffrey Zhao      2007-01-25 14:59
    @Cat Chen
    还好,一般不用id。
      回复  引用  查看    
  32. #32楼 aw[未注册用户]2007-01-25 17:38
    我很好奇, 为什么要在head里加入
    runat = server,干吗用的?
      回复  引用    
  33. #33楼[楼主] Jeffrey Zhao      2007-01-25 18:03
    @aw
    为了能够在服务器端操作Head,比如设置title。
      回复  引用  查看    
  34. #34楼 有点麻烦[未注册用户]2007-01-25 22:24
    想问大家个问题
    在用Forms Authentication时如果在web.config中加入authentication验证的话在脚本调用方法时会发生缺少对象。
    不知道大伙有没有人碰到这个问题的
    附上官方doc地址:http://ajax.asp.net/docs/tutorials/UsingFormsAuthenticationTutorial.aspx
      回复  引用    
  35. #35楼[楼主] Jeffrey Zhao      2007-01-25 23:20
    缺少什么对象啊?
      回复  引用  查看    
  36. #36楼 stonezhu      2007-01-26 00:26
    真是受教了,谢谢"老赵"
      回复  引用  查看    
  37. #37楼 stonezhu      2007-01-26 00:37
    再问一个和主题不相干的问题,希望老赵能帮帮忙
    我最近在做AJAX应用时,遇到一个图片高宽处理的问题,就是怎么样才能立刻获得我一个图片的WIDTH,HEIGHT.然后再把WIDTH,HEGITH传给一个方法进行计算,生成的一个缩略图的WIDTH,HEIGHT.
    我现在是用这样的一个方法:
    var tmpImage = document.createElement("IMG");
    tmpImage.src="http://image">http://image.***.com/ImageServer/***/my.jpg";
    var tmpArr = GetImgSize(tmpImage.Width,tmpImage.Height);
    这样得到一个Width,Height
    但是这样的方法存在一个严重的问题,这样很可能tmpImage这个时候还没有加载到Client端,这样导致我传进"GetImgSize()"方法的参数没有值.
    我现在还不知道该怎么解决这样的问题,希望能指点迷津啦,
    Thank you
      回复  引用  查看    
  38. #38楼[楼主] Jeffrey Zhao      2007-01-26 00:41
    @stonezhu
    您必须在它的onload事件中才能得到width和height
    var img = document.createElement('img');
    img.onload = function(){ alert(this.width); alert(this.height); }
    img.src = 'my.jpg';
      回复  引用  查看    
  39. #39楼 stonezhu      2007-01-26 00:45
    @Jeffrey Zhao
    那你的意思是我只有这它的onload事件中得到它的width,height然后再进行相关计算吗?
    谢谢,你睡的还挺晚的,这么晚还回复我,
    TNANK YOU VERY MUCH:)
      回复  引用  查看    
  40. #40楼[楼主] Jeffrey Zhao      2007-01-26 00:50
    @stonezhu
    因为只有在onload事件被触发才表示图片被加载完了。
    // 我睡得很晚,所以没关系的。:)
      回复  引用  查看    
  41. #41楼 stonezhu      2007-01-26 00:54
    @Jeffrey Zhao
    :)明白了,谢谢.搞程序的多数晚睡...
    最近一直在做AJAX应用,用的Prototype.
    以前对这方面不太了解.
    那我的相关方法都要在onload=function(){...};这个里面进行喽.
      回复  引用  查看    
  42. #42楼 stonezhu      2007-01-26 00:59
    @Jeffrey Zhao
    谢谢啦,我要休息了,你年纪也很轻,早点休息喽,苦命本钱嘛...
    Goog night
      回复  引用  查看    
  43. #43楼[楼主] Jeffrey Zhao      2007-01-26 01:07
    @stonezhu
    没错,必须这么做的。
    我晚睡是因为喜欢阿,搞技术很爽的。:)
      回复  引用  查看    
  44. #44楼[楼主] Jeffrey Zhao      2007-01-26 15:39
    safari下没有load事件或者readystatechange事件,因此需要调用一个notifyScriptLoaded()方法。
      回复  引用  查看    
  45. #45楼 萝卜[未注册用户]2007-01-29 11:27
    动态加载脚本早前我就在使用这种方法了,类似于利用一个公共模块管理的方法,通过自定义的语句引入需要加载的包。
    但是带来的问题就是,如果大量采用这种方法,因为加载的过程是异步的,所以在使用加载的包中的内容的时候,总是需要先判断其加载状态,这点比较麻烦而且感觉不太好控制。
      回复  引用    
  46. #46楼[楼主] Jeffrey Zhao      2007-01-29 11:58
    @萝卜
    为什么说麻烦呢?有onload和onreadystatechange事件,都是比较容易控制的。:)
      回复  引用  查看    
  47. #47楼 萝卜[未注册用户]2007-01-29 17:49
    麻烦的地方在于:要做到按需求加载模块.并不完全是页面初始化的时候.也可能发生在用户的操作过程中。
    我设定一个加载的触发条件,用户在操作到某一步的时候执行这个加载,而加载过程是异步的,需要的时间未知,我需要在加载完成后马上完成一系列操作,而剩下的一系列操作又受这一部分操作的影响。这样,在编写代码的时候,只要是涉及到动态加载的包中的内容的部分,都需要预先进行状态判断,而后用类似timeout或者interval的方式来处理以保证后续操作能够自动完成.当代码量大了之后就会变的异常烦琐而且感觉上不好控制。
    不知道我表述是否清楚,又或者我的处理方法不够精明。我只是觉得一开始就把所需要的包全部加载并不是很好的办法,最好是在用户实际的操作过程中按需要来加载.
      回复  引用    
  48. #48楼[楼主] Jeffrey Zhao      2007-01-29 18:09
    @萝卜
    如果可以的话全部加载一般也没有什么问题。除非是很明显的“按需”加载需求。
    我还是没有理解setTimeout或setInterval的作用是什么,不是加载完成有onload或者onreadystatechange回调函数吗?即使错误了也有onerror回调函数的说。
    // 支持safari回麻烦些,因为没有onload或onreadystatechange。
      回复  引用  查看    
  49. #49楼 萝卜[未注册用户]2007-01-29 21:31
    setTimeout,setInterval起到跟onload及onreadystatechange同样的作用,更兼容的方法吧,就是判断加载是否完成,如果完成则执行语句,否则则等待一定时间间隔重新判断。
      回复  引用    
  50. #50楼 horsefaced[未注册用户]2007-08-31 14:15
    document.write('<script type="text/javascript" language="javascript" src="' + src + '"></script>');

    实际中,这么一句就可以了,不用给head加id, 也不用写什么,嗯,google就是这么干的。当时文章中所说的加载顺序什么的问题也还是继续存在的。
      回复  引用    
  51. #51楼 ╃小〥斌╄      2007-08-31 14:27
    可以把src 指向到一个action , action里是动态生成的该页面所需要的js。
    不光js ,css 也同样需要优化的。
      回复  引用  查看    
  52. #52楼[楼主] Jeffrey Zhao      2007-08-31 14:45
    @horsefaced
    对FireFox没有效果,我这篇文章就是为了解决FireFox的问题的。
      回复  引用  查看    
  53. #53楼[楼主] Jeffrey Zhao      2007-08-31 14:46
    @╃小〥斌╄
    的确可以这样,不过这个和优化有什么关系呢?
      回复  引用  查看    
  54. #54楼 ╃小〥斌╄      2007-08-31 15:11
    优化不光是速度 性能 还有安全吧。

    这样做可以防止用纯src 来下载js,css 。 屏蔽非本站发出的请求。
      回复  引用  查看    
  55. #55楼[楼主] Jeffrey Zhao      2007-08-31 15:27
    @╃小〥斌╄
    呵呵,那么就是吧。:)
      回复  引用  查看    
  56. #56楼 horsefaced[未注册用户]2007-08-31 16:40
    不好意思,我说的那个方法是可以的,我刚刚在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. #57楼[楼主] Jeffrey Zhao      2007-08-31 21:36
    @horsefaced
    呵呵,其实我这个系列的(1)就讲了这个内容,不过我当时试验的结果是不成功的阿。
      回复  引用  查看    



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 629733




相关文章:

相关链接: