JAL – JavaScript 的另一种加载器

很久以前,我看了汤姆·克鲁斯、布拉德·皮特和克尔斯滕·邓斯特主演的电影“夜访吸血鬼”。给我印象最深的一个场景是,皮特饰演的角色意识到莱斯特正在利用他来适应当今时代。对于开发者来说,这不是一个坏规则。事实上,它非常好。如果你想跟上潮流并保持领先地位,跟随前沿技术,进行实验并复制他人的做法。逆向工程和重新发明轮子是一种幸福。将其应用于开源,我们——开发者、黑客、设计师——手中拥有大量工具。只需想想 Web 浏览器中的“查看源代码”。没有它,我们不可能达到今天的水平。复制就是学习。没有站在前人的肩膀上,发明是不可能的。

我工作的公司,Tail-f Systems 最近刚刚开源了一个名为 JAL 的小型 JavaScript 库,它是“Just Another Loader”的首字母缩写。这是一个初生的项目,缺少某些功能,但可以胜任工作并且做得很好。顾名思义,它是一个用于并行条件依赖加载资源文件的工具。我们在我们的 Web UI 中使用它来加载脚本和 CSS 文件。它存在的唯一原因是:为了加快速度!

我们测试了 YepNope,这是一个很棒的加载器,但感觉它可以更快。它还有一些我们并不需要的功能。所以我们自己编写了一个。我们重新发明了轮子。这能有多难?好吧,这相当困难。

我们需要一个资源加载器,它不仅可以加载 JavaScript,还可以加载样式表。它还需要能够并行加载和分组加载资源以处理依赖关系,例如在加载 jQuery 插件之前加载 jQuery。最后一个要求是条件加载,即如果浏览器缺少原生 JSON 支持,则加载 JSON.js。

并行依赖加载

典型的设置如下所示

$loader
    .load('js/shape.js')
    .load([
          'js/circle.js'
        , 'js/rectangle.js'
    ])
    .load('js/square.js')
    .ready(function() {
        // Start app
    })

设置了三个依赖组。第一个加载一个形状。第二个加载一个圆形和一个矩形,它们依赖于形状。最后一组包含一个正方形,它派生自一个矩形。在这个简单的例子中,速度提升发生在第二个组,因为圆形和矩形是并行加载的。现在,想象一下,您的应用程序中有大量具有不同依赖关系的脚本。传统的方法是将所有脚本连接到一个大型捆绑包中,然后压缩该捆绑包。您实际上是在以旧的方式加载脚本,一个接一个地加载。现代浏览器能够并行加载脚本和资源。它们实际上会打开到 Web 服务器的多个连接,并同时加载多个资源。因此,如果您有一个脚本需要 5 秒才能加载,并且您将其分成 5 部分并并行加载这些部分,那么加载时间理论上将变为 1 秒。这比之前快了五倍!

条件加载

现在来看条件加载。条件加载是在满足特定条件时加载资源。浏览器是否具有原生 JSON 支持?没有?好吧,我们会解决这个问题!这是一个加载 JSON polyfill 的示例

$loader
    .when(typeof window.JSON === 'undefined', function(loader) {
        loader.load('js/json.js')
    })

已完成

一旦资源组加载完毕,JAL 允许您执行代码。这是一个示例,其中 jQuery 中的“ready”事件在所有脚本加载完毕之前都会暂停。

$loader
    .load('js/jquery.min.js')
    .done(function(){
        // Stop jQuery from triggering the "ready" event
        $.holdReady(true)
    })
    .load([
          'js/script-one.min.js'
        , 'js/script-two.min.js'
    ])
    .ready(function() {
        // Allow jQuery to trigger the "ready" event
        $.holdReady(false)
        // Start app
    })

它是如何完成的

编写 JAL 既具有挑战性又很有趣。最困难的部分是确保组之间保持加载顺序。这很棘手,因为事情发生得很快,并且浏览器之间的性能差异很大。

JAL 使用资源队列和轮询函数实现。在资源组加载完毕之前,队列处于锁定状态。加载完成后,会触发“done”事件。如果您需要,这允许您将一个或多个资源组注入到队列的前面。触发“done”事件后,队列将解锁,并且轮询器可以自由加载下一个资源组。

一旦加载器序列执行完毕,轮询器本身就会启动。这是通过使用超时时间为 0 毫秒的 setTimeout 将轮询器推送到脚本堆栈顶部来完成的。这是 Web 浏览器 JavaScript 引擎单线程模型如何使用的经典示例。

结语

您是否有一个大型连接的 JavaScript 文件?它是否已压缩并进行了 gzip 压缩?它加载速度快吗?您想要更快吗?然后分别压缩并进行 gzip 压缩您的资源文件,并改用条件并行依赖加载器。

关于 Helgi Kristjansson

Helgi Kristjansson 是一位来自冰岛的 JavaScript 维京人。他于 1998 年入侵瑞典,在那里他主要从事与硬件相关的行业中基于 Web 的用户界面的开发工作。他目前的雇主是 Tail-f Systems,一家位于斯德哥尔摩的公司。您可以在 Twitter 上通过 @djupudga 联系 Helgi。

Helgi Kristjansson 的更多文章……


7 条评论

  1. Colin Jack

    “您是否有一个大型连接的 JavaScript 文件?它是否已压缩并进行了 gzip 压缩?它加载速度快吗?您想要更快吗?然后分别压缩并进行 gzip 压缩您的资源文件,并改用条件并行依赖加载器”

    想知道您对此进行了哪些测试?

    例如,在实践中您获得了多少速度提升,以及在使用这种方法与移动设备使用的网站时是否遇到任何问题?

    2012 年 11 月 22 日 04:54

  2. Johan

    这仅在允许对服务器进行足够多的并行请求并且用户拥有足够的带宽的情况下才有效,否则发出多个 HTTP 请求的开销将实际上使它比将所有内容放在一个文件中 _更慢_。

    2012 年 11 月 22 日 05:24

  3. Helgi Kristjansson

    Colin:我在此声明中的假设是,人们正在以传统方式在网页上包含脚本。速度提升取决于许多因素。其中之一是,通过使用资源加载器,您不会阻塞页面其余部分的加载。另一个是可用带宽、Web 服务器设置,当然还有访客使用的 Web 浏览器类型。然后还有用户感知到的速度提升问题。我们没有对应用程序中的速度提升进行任何大量测试——我们的产品没有那么严格的要求——驱动因素是我们对依赖加载的要求,为此,我们基于像 Steven Souder 这样的人一直在撰写的内容。此外,我们不是网络商店。Tail-f 是一家产品开发公司。

    它可以在 iPad 和 iPhone 4 上正常工作,没有任何问题。

    Johan:您完全正确。用例和目标受众很重要。在我们的产品中,我们大约有 5 MB 未压缩的 JavaScript,我们也可以指定我们支持的浏览器。但是,如果您有相当数量的脚本并且可以控制 Web 服务器,则依赖加载可能是一种可行的替代方案。

    2012 年 11 月 22 日 07:17

  4. Steve Souders

    我在评估脚本加载器时提出的第一个问题是“脚本加载器本身是否会阻塞页面?”不幸的是,JAL 是同步加载的——它会阻止页面呈现。具有讽刺意味的是,一个旨在促进异步行为的脚本加载器本身是同步的。在每个 HTML 文档中内联 JAL 的 7K JS 的替代方案也会损害性能。ControlJS 是一个异步加载自身脚本加载器的示例。我希望看到更多加载器遵循这种模式——我们最不需要的就是页面中的每个脚本都被同步脚本阻塞。

    2012 年 11 月 22 日 11:49

  5. Helgi Kristjansson

    JAL 的异步加载是一个好主意,感谢您的意见。但是,对于我正在开发的产品,我相信延迟执行(如 ControlJS 中那样)将产生最大的速度改进。可以使应用程序启动得更快。感谢您的宝贵意见。

    2012 年 11 月 22 日 13:13

  6. JorisW

    我一直在服务器端这样做。所有 CSS 和 JavaScript 代码都连接到两个大文件中(预编译)。仅包含在结果文件中才能满足依赖关系的脚本。
    使用属性 async 包含生成的 javascript 文件。

    我已经看到使用这种方法获得了许多速度改进。

    2012 年 11 月 23 日 07:52

  7. Brett Zamir

    虽然这看起来做得很好,但我很好奇为什么如果您知道 https://requirejs.node.org.cn 的话,您没有使用它?这似乎是一个使用虚拟标准模块加载方法经过深思熟虑的项目。

    诚然,它没有您用于条件加载的漂亮语法——尤其是在需要使用浏览器不断发展的情况下使用 shim 的世界中。(我在 https://github.com/jrburke/requirejs/issues/520#issuecomment-10834588 中请求了此功能)

    2012 年 11 月 28 日 21:27

本文的评论已关闭。