65.9K
CodeProject 正在变化。 阅读更多。
Home

7个技巧,让加载JavaScript丰富的Web 2.0网站显著更快

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (39投票s)

2009年9月23日

CPOL

14分钟阅读

viewsIcon

95478

了解 Microsoft 新的 Doloto 原理以及我在 Pageflakes 中加载大量 JavaScript 而不影响性能的其他 6 种很酷的技术。

引言

当你创建丰富的Ajax应用程序时,你会使用外部JavaScript框架,并且有自己编写的代码来驱动你的应用程序。众所周知的JavaScript框架的问题在于,它们提供了丰富的功能集,而这些功能并非总是完全必需的。你可能只使用了jQuery的30%,但你仍然下载了完整的jQuery框架。因此,你下载了70%不必要的脚本。同样,你可能写了自己的JavaScript,但并非所有这些脚本都会被使用。当网站首次加载时,可能有些功能**不**会被使用,导致初始加载时下载了不必要的脚本。初始加载时间至关重要——它可以成就或毁掉你的网站。我们进行了一些分析,发现初始加载时间每增加500毫秒,我们就会损失大约30%的流量,这些流量永远不会等待整个页面加载,而是直接关闭浏览器或离开。因此,节省初始加载时间,即使只是几百毫秒,对于初创公司的生存至关重要,尤其是对于丰富的AJAX网站。

你一定注意到了微软的新工具Doloto,它有助于解决以下问题

像GMail、Live Maps、Facebook等现代Web 2.0应用程序,使用动态HTML、JavaScript和其他Web浏览器技术的组合,通常被称为AJAX,将页面生成和内容操作推送到客户端Web浏览器。这提高了这些网络密集型应用程序的响应速度,但将应用程序执行从后端服务器转移到客户端也常常会急剧增加必须首先下载到浏览器的代码量。这就产生了一个不幸的困境:为了创建响应式的分布式Web 2.0应用程序,开发人员将代码移到客户端,但为了使应用程序响应迅速,代码必须首先传输到那里,这需要时间。

微软研究院研究了这个问题,并在2008年的这篇研究论文中展示了,如果能够将JavaScript框架分成两部分——一部分是页面初始渲染绝对必需的主要部分,另一部分是初始加载不必需、可以稍后或按需下载的辅助部分——那么初始加载可以取得多大的改进。他们研究了我之前的创业公司Pageflakes,并报告说:

2.2.2 动态加载:Pageflakes 与 Bunny Hunt 形成对比的是 Pageflakes 应用程序,这是一个工业级的内容聚合页面,提供门户类的功能。尽管Pageflakes的下载大小超过1MB,但其初始执行时间却相当快。检查网络活动发现,Pageflakes在初始页面加载时只下载了一小段代码,并在后台动态加载其余代码。正如Pageflakes所演示的,开发人员现在可以使用动态代码加载来提高其Web应用程序的性能。然而,设计一个易于动态代码加载的应用程序架构需要仔细考虑JavaScript语言问题,如函数闭包、作用域等。此外,将代码分解为动态加载组件的最优拆分通常需要开发人员忽略代码的语义分组,而主要考虑函数的执行顺序。当然,代码的演进和用户工作负载的变化使得这两者都成为软件维护的噩梦。

早在2007年,我就在寻找提高初始加载时间、减少用户流失的方法。随着我们引入新的酷功能,不愿意等待页面加载而离开的用户数量与日俱增。这令人惊讶。我们认为新功能会留住更多用户,但结果恰恰相反。分析表明,导致用户流失的原因是初始加载时间,而不是用户保留。因此,我们所有的辛勤工作都基本上白费了,我们必须想出一些突破性的方法来解决这个问题。当然,我们已经尝试了所有基本的东西——IIS压缩浏览器缓存、用户操作时的按需加载JavaScript、CSS和HTML延迟JavaScript执行——但都没有奏效。框架和我们自己编写的框架实在太大了。所以,一个想法闪过我的脑海,如果我们可以分两步加载一个类中的函数,会怎么样?第一步加载类以及绝对必需的函数,第二步向现有类注入更多函数。

将类拆分成多个JavaScript文件

这是我的想法

var VeryImportantClass = {
    essentialMethod: function()
    {
        DoSomething1();
        DoSomething2();
        DoSomething3();
        DoSomething4();
    },
    notSoEssentialMethod: function()
    {    
        DoSomething1();
        DoSomething2();
        DoSomething3();
        DoSomething4();
    }    
};

这里你看到一个类,在初始加载时必须加载。如果这个类没有在初始加载时加载,页面加载就会因为JavaScript错误而中断。现在,假设这个类非常大。它有许多核心函数,提供了你网站的大部分重要功能。然而,并非这个类中的每个函数都在初始加载时被调用。有些函数在用户执行某些操作时才会被调用,比如点击某个按钮或将鼠标悬停在某个菜单上。所以,我们可以将那些函数提取到另一个JavaScript文件中,并在页面渲染完成后加载。这意味着,你需要将类更改为这样:

var VeryImportantClass = {
    essentialMethod: function()
    {
        DoSomething1();
        DoSomething2();
        DoSomething3();
        DoSomething4();
    }    
};

这里notSoEssentialMethod被从JavaScript文件中提取出来。我们称这个精简后的JavaScript为PreFramework.jsPreFramework必须在页面渲染开始前加载。它会阻塞页面渲染,直到完全加载。因此,它必须只包含最少的代码。

现在,你可以在页面中添加另一个JavaScript,使用defer属性加载,并将其放在页面末尾,在该JavaScript中,你需要写成这样:

VeryImportantClass.notSoEssentialMethod = function()
{
    DoSomething1();
    DoSomething2();
    DoSomething3();
    DoSomething4(); 
}

我们称这个JavaScript文件为PostFramework.js。由于这个JavaScript是使用script标签上的defer属性加载的,它不会阻塞页面渲染。它在浏览器完成脚本加载后加载。

PostFramework.js加载时,它会将附加的方法添加到同一个类中。这样,你的VeryImportantClass就可以按需加载更多的行为。所有使用VeryImportantClass的代码保持不变。根本不需要修改它们。由于这些代码只在用户执行某些操作时才会被执行,它们在初始加载时不会被触发,所以即使函数不存在也不会有问题。JavaScript不像C#那样,如果某个函数不存在就会编译失败。所以我利用了这一点,显著减少了我们初始加载的JavaScript量。这正是微软研究院在Doloto工具中使用的技术。Doloto会自动为你完成这一切。事实上,它更聪明。它会做一些称为“存根”(Stubbing)的事情。请继续阅读。

为未在初始加载时调用的函数创建存根

在Pageflakes,由于小部件是由外部开发人员和公司开发的,很难跟踪JavaScript。因此,不可能扫描我们所有的JavaScript并确定哪些函数是最初被调用的,哪些不是。当我们积极地将函数从初始加载中移除时,我们开始在鼠标移动、工具提示或有时窗口大小调整时遇到JavaScript错误,因为这些事件所需的脚本不存在。为了找出在PostFramework.js加载之前会被调用的JavaScript,这是一个漫长的试错过程。不同的互联网带宽速度造成了更大的麻烦,因为我们无法假设PostFramework.js会在10秒内加载完成,就可以将FunctionX从Pre移到Post。

所以,我尝试了另一种方法。我保留了函数声明,但将函数体内的代码移走了。这避免了JavaScript错误,因为你可以调用这些函数,但什么都不会发生。结果是,在PreFramework.js中:

var VeryImportantClass = {
    essentialMethod: function()
    {
        DoSomething1();
        DoSomething2();
        DoSomething3();
        DoSomething4();
    },
    notSoEssentialMethod: function()    
    {
        // No code here
    }
};

PreFramework只是声明了函数,以便其他人可以调用它们,但什么都不会发生。这被称为存根。PostFramework将代码注入到该存根中。这解决了大部分JavaScript问题,除了那些期望函数返回某个值但函数未返回任何值而导致错误的。但这种情况非常少见。

因此,我们将超过1MB的JavaScript拆分成了Pre和Post框架。PreFramework只有大约200KB,包含了巨大的Microsoft AJAX库以及我们自己绝对必需的用于初始页面渲染的JavaScript。其余0.9MB的JavaScript被移到了Post框架。

现在有了Doloto,它会做同样的事情,但会自动完成,这样你就不必手动选择函数了。可惜它在我们失去流量和业务的时候还没有出现。如果Doloto早两年出来,我们会更成功。Pageflakes就是一个真实的例子,Doloto可以在两年内将其估值至少提高500万美元。

然而,我还有比Doloto现在所做的更厉害的技巧。Doloto会对你的代码进行科学分析——它会检查JavaScript函数何时被调用,并基于此找出哪些脚本在页面渲染之前被调用,哪些脚本在页面渲染之后被调用。它还会记录函数调用的时间,以便创建相对接近调用的函数组,帮助你进一步将JavaScript拆分成小集群。但是科学分析并不总是最优的。你可能编写了尝试初始化工具提示、菜单、手风琴、显示/隐藏div、调整div大小等代码,并把它们放在脚本的靠前位置。Doloto看到它们在初始加载时被执行,并将它们保留在PreFramework中。然而,你知道它们并不是页面渲染的先决条件。你可以很容易地按需完成这些操作,在页面加载完成后。例如,当用户将鼠标悬停在某些链接上查看工具提示时,或者当某个按钮被点击时,你可以初始化工具提示。这是只有人类大脑才能决定的,系统无法预测。但这可以节省大量的下载时间。

文本中的JavaScript代码

你可以通过执行一些eval或使用新的Function(‘’)语法来惰性执行JavaScript。这样的JavaScript在创建时不会被执行,而是在被调用时执行。例如,而不是这样做:

function initTooltips()
{
    var links = document.getElementsByTagName("a");
    for (a in links)
    {
        a.onmouseover = show_tooltip;
    }
}

这要求show_tooltip函数或至少它的存根已经可用。而不是这样做,如果你使用eval或Function方法:

function initTooltips()
{
    var links = document.getElementsByTagName("a");
    for (a in links)
    {
        a.onmouseover = new Function("if (typeof show_tooltip == 'function') 
                                       show_tooltip(this)");
    }
}

当这段代码运行时,不需要show_tooltip函数。如果这个函数没有加载,并且用户将鼠标悬停在链接上,它会忽略调用函数。只有当你的庞大的Post框架加载完成,并且show_tooltip函数可用时,它才会调用该函数。从UI的角度来看,用户可能在最初的5秒内看不到工具提示。但谁在乎呢!为了缩短初始页面加载时间(从而避免用户流失),牺牲一些非必要的特性是更好的选择。

将UI加载分解为多个阶段

由于Pageflakes是一个非常重量级的AJAX网站,大部分页面是通过AJAX调用使用JavaScript渲染的。这意味着内容和行为都是异步加载的。例如,当你第一次访问我的Pageflakes Pagecast时,你会看到这个画面一段时间:

image

这里你看到的是内容的骨架或占位符。从服务器端,我们知道最终会交付什么给浏览器。我们不直接交付内容和JavaScript,而是先交付一个假的骨架,同时下载必要的JavaScript来渲染实际内容。虽然这也会增加实际加载时间,因为假的骨架也需要一些带宽,但它被保持在绝对最小的范围内。由于用户看到的是骨架而不是空白页面,用户会觉得页面加载得更快。我们进行了充分的用户研究,得出的结论是,如果我们能尽快给用户提供真实内容的预览,而不是进度条,用户会感觉网站运行得更快。

一旦骨架交付,内容和使其工作的内容的JavaScript将在后台交付。当它们完全交付后,我们会一次性渲染尽可能多的内容,以避免频繁的重新渲染和页面重组。

image

你将从MIX 09的这个视频中了解到类似的技巧——构建高性能Web应用程序和网站

始终从上到下地增加内容,绝不收缩或跳跃

你渲染页面内容的方式对用户感知网站速度有显著影响。如果你渲染内容的方式导致可见内容时而上下移动,那么用户对网站速度的感知会变差。在Pageflakes,最初我们让小部件在初始时显示为一个特定的高度,足以显示“正在加载…”的消息。当页面最终用真实内容渲染时,有些小部件会变大,有些会变小,有些会上下跳跃。这似乎表明页面非常繁忙,让用户感到恼火。你会在我的开源AJAX起始页Dropthings上看到类似的问题。当页面加载时,网站似乎花费大量时间来渲染页面内容。这给用户一种加载缓慢的感觉。但如果你去我的Pageflakes页面,你会看到内容是连续流动的。小部件只朝一个方向增长,即垂直方向,这是自然的体验。并且页面内容有一种整体微妙的加载感,小部件和谐地一个接一个地加载,不相互干扰,从上到下 nicely 地构建页面。这种平滑的内容加载流程让用户感觉加载速度更快,尽管实际加载时间由于我们为创造这种体验所玩的所有技巧而有所增加。

image

上图显示小部件只朝一个方向增长和移动。这比一些小部件收缩、一些增长更能给人一种加载更快的感觉。小部件骨架的大小是经过大量研究确定的,以找到最能产生“增长”效果的大小,特别是对于包含大量RSS Feed的页面。然而,正如你所注意到的,常规的Pageflakes主页加载效果并不那么令人印象深刻,因为它包含太多不同类型和大小的小部件。

从服务器交付特定于浏览器的脚本

当你构建丰富的Ajax客户端时,你必须处理特定于浏览器的CSS和JavaScript。尤其是在Internet Explorer 6和Internet Explorer 7时代,还有Firefox、Safari、Opera和Chrome等浏览器,它们似乎都有自己的渲染风格,并且渲染方式不同。结果,你的CSS和JavaScript会因为浏览器特定的调整而膨胀。例如:

function browserSpecificStuff()
{
    if (browser.isIE)
    {
        DoSomething1();
        DoSomething2();
        DoSomething3();
    }
    else if (browser.isFirefox)
    {
        DoSomething4();
        DoSomething5();
        DoSomething6();
    }
    else if (browser.isSafari)
    {
        DoSomething3();
    }
}

如你所见,Safari用户将加载80%的无用脚本。所以,你需要找到一种方法将这些脚本拆分成特定于浏览器的文件。将通用函数放在一个通用的JS文件中,但将特定于浏览器的函数提取出来,并将它们分开到特定于浏览器的JavaScript文件中。加载JavaScript时,先加载通用的,然后加载一个特定于浏览器的,该脚本将特定于浏览器的实现注入到通用的JS中。

我们处理此问题的方式是使用服务器端HTTP处理程序,该处理程序会查看浏览器用户代理,检测浏览器类型,并仅加载包含特定浏览器相关代码的脚本。例如,我们将框架代码分成不同的块,如PreFramework.Common.js,然后是PreFramework.IE.jsPreFramework.Firefox.jsPreFramework.Safari.js。我们创建了一个处理程序,它将PreFramework.Common.js与一个特定于浏览器的PreFramework.XXXX.js结合起来,并发出合并后的输出。这样,浏览器只会获得相关的脚本,不多余。这节省了下载时间,加快了页面加载速度,留住了更多用户,提高了你的初创公司的估值。这种简单的技巧有时能让你口袋里多出一百万美元!

结论

第一次访问页面的加载性能对你的业务至关重要,如果留住用户是衡量你业务成功的首要标准。你的页面加载越快,用户流失就越少。如今,用户比ISDN线路和拨号调制解调器时代更加不耐烦。所以,任何加载时间超过3秒的网站都会有大约30%的用户流失。因此,你必须尽一切可能节省初始加载的毫秒数,并确保初始渲染尽可能在视觉上令人愉悦。

Burn! kick it Shout it 

历史

  • 2009年9月23日:初始版本

© . All rights reserved.