你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
leonli

(译) 异步碎片化:重新探索Progressive HTML渲染引擎 2个回复 专栏 @ 架构

leonli 发布于 1 年前

原文作者: Patrick Steele-Idem
原文地址:http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/
原译文地址:http://piliyu.com/marko-progressive-html/
译者:Leon

本博文译文版权归译者所有, 转载请注明出处.

在eBay, 我们高度重视网站的性能和加载速度, 我们总是希望开发人员能够开发出被高速加载的网页. 这一点促使我们需要很好的了解网页内容是如何分发内容给浏览器的. Progressive HTML渲染技术的应用在优化我们的网站上来讲已经是一个相对来说比较陈旧的话题了,但是它却在目前越来越多的新技术中被淹没了. 这项技术很简单,就是早点刷新(flushing)与浏览器之间建立的数据流,并且在结束前刷新多次. 浏览器本来就拥有很好的从服务器端解析和响应HTML数据流的能力(这里指的是在响应已经结束之前). 这个功能允许HTML和外部的资源文件(CSS, JS等)被提早的下载, 并且提前渲染一部分的页面. 这样一来,实际加载时间和感觉上的加载时间都得到了提高.

在本篇博文中,我们将会深入了解这项技术,我们叫它:"异步碎片化(*Async Fragments*)", 它将会使用Progressive HTML的概念来做到非常快捷简易的提高网站页面响应的速度. 这里的例子将会用到 Node.js, Express.jsMarko模板引擎 (*Marko是一个支持流(Streaming), 数据刷新(Flushing)和异步渲染(Asynchronous Rendering)的Javascript模板引擎*). 即时你不使用以上这些技术,这篇博文也会让你从核心概念上帮助到你.

历史背景

Progressive HTML渲染在这篇2005年由Jeff Atwood发布的博文The Lost Art of Progressive HTML Rendering中就有过探讨. 另外,雅虎(Yahoo)公司的性能团队在他们的一份指南Best Practices for Speeding Up Your Web Site中, 也曾发布过关于"提早刷新缓存(*Flush the Buffer Early*)"的相关规则. Stoyan Stefanov提供了一篇叫做Progressive rendering via multiple flushes的博文,深入讲解Progressive HTML技术. 脸谱(Facebook)公司也探讨过他们怎样使用他们的"BigPipe"技术来提高页面的相应时间以及实际的性能上的感受,他们的做法是把页面分割为"pagelets". 这些文章中所使用到的技术和观点也是本片博文的灵感所在.

没有Progressive HTML渲染

页面的渲染将会比较缓慢如果没有使用progressive HTML的方式,这是因为所有的字节(bytes)直到相应的请求结束后才会被刷新(flushing). 另外,当客户端最后收到了完整的HTML后,它才会开始下载一些额外的静态资源(例如: CSS, Javascript, 字体文件以及图片), 并且下载这些其他的资源将会需要额外的开销. 再者,如果页面不使用Progress HTML技术还会造成用户感知的页面读取时间过长, 因为在浏览器屏幕不会发生变化直到完整的HTML被下载完成并且<head>中的CSS和字体文件被下载完成. 在没有Progressive HTML渲染的时候,一个客户端/服务器端的瀑布模型图将会如下所示:

假定对应的控制层如下:

function controller(req, res) {
    async.parallel([
            function loadSearchResults(callback) {
                ...
            },
            function loadFilters(callback) {
                ...
            },
            function loadAds(callback) {
                ...
            }
        ],
        function() {
            ...
            var viewModel = { ... };
            res.render('search', viewModel);
        })
}

这里我们可以看到,页面只有当所有的异步数据都加载完成后才会被渲染.

这是因为HTML没有被刷新(flushing)直到所有的服务器端服务执行完成, 这样一来,用户将会持续在一开始的时候看到一个空白的页面很久, 这将会造成很不好的用户体验(尤其处于网络不好,或者后台服务器执行很慢的时候). 但实际上我们可以通过提前刷新(flushing)数据流来提供更好的体验.

提前刷新头部

一个很简单的技巧来提高网站的相应体验就是通过刷新(flushing)页面的<head>. 因为<head>中包含了一些必要的外部资源(例如<link>标签), 以及标题栏和导航, 这样将会使得外部CSS被很快的下载并且浏览器开始画出初始的页面,如下图所示:

如上图所示,刷新(flushing)头部将会减少相应时间并渲染出初始页面. 这种方式提高了页面的响应速度, 但是却没有明确的减少整个页面变得完全可用所花费的时间. 在使用这种方式时,服务器还是在等待所有的后端服务完成后才会刷新(flushing)最终的HTML. 另外,下载外部的Javascript资源将会延迟至<script>标签在页面的最后被渲染出来之后.

多重刷新(Flushes)

相对于只是前提刷新头部,更有益的方式无疑是在相应结束前做多重刷新(*Multiple flushes*). 实际上,一个页面可以被分割为多个碎片(fragments), 并且这些碎片中的一部分可能依赖于异步加载的后端服务,但其他的可能不是依赖于任何的后台数据. 这些依赖于后端加载的异步数据就应该被异步化的加载并尽可能快的刷新(flushing)并渲染.

目前来说,我们可以确定的是这些碎片需要被按照HTML属性的顺序来做刷新(可是数据却是异步加载的), 但是我们需要解决的是怎样可以让无序(out-of-order)的刷新(flushing)可以用来解决页面的加载时间和用户实际感受的时间. 当使用有序(in-order)刷新时,由于碎片是完全无序的所以需要被缓存起来直到他们已经准备好被刷新给有序的HTML属性.

译者注:这里比较绕,如果不明白就接下往下看.

异步碎片的有序刷新(in-order)

举例说明, 让我们假定我们有一个被分割的复杂页面有一下这些碎片(fragments):

每一个碎片都基于他们将会在文档上出现的顺序安排了一个编号. 我们的HTML代码输入可能如下所示:

<html>
<head>
    <title>Clothing Store</title>
    <!-- 1a) Head <link> tags -->
</head>
<body>
    <header>
       <!-- 1b) Header -->
    </header>
    <div class="body">
        <main>
            <!-- 2) Search Results -->
        </main>
        <section class="filters">
            <!-- 3) Search filters -->
        </section>
        <section class="ads">
            <!-- 4) Ads -->
        </section>
    </div>
    <footer>
        <!-- 5a) Footer -->
    </footer>
    <!-- 5b) Body <script> tags -->
</body>
</html>

Marko的模板引擎提供了一种方式可以直接绑定模板碎片给后端的异步数据提供者的函数(function)或者是Promise对象. 某一个异步的碎片将会在异步数据提供的回调函数被调用时渲染. 如果说这个异步碎片已经准备好被刷新(flushing), 那么它将会立即被刷新到流(streaming)中. 否则, 如果异步碎片在完成时是无序的,那么被渲染的HTML将会被缓存起来直到它也已经准备好被刷新. Marko的模板引擎将会最终确保所有的碎片都是按照HTML的属性顺序来刷新的.

继续上面的例子,我们的HTML页面的模板附上异步碎片后将会如下所示:

<html>
<head>
    <title>Clothing Store</title>
    <!-- Head <link> tags -->
</head>
<body>
    <header>
        <!-- Header -->
    </header>
    <div class="body">
        <main>
            <!-- Search Results -->
            <async-fragment data-provider="data.searchResultsProvider"
                var="searchResults">

                <!-- Do something with the search results data... -->
                <ul>
                    <li for="item in searchResults.items">
                        $item.title
                    </li>
                </ul>

            </async-fragment>
        </main>
        <section class="filters">

            <!-- Search filters -->
            <async-fragment data-provider="data.filtersProvider"
                var="filters">
                <!-- Do something with the filters data... -->
            </async-fragment>

        </section>
        <section class="ads">

            <!-- Ads -->
            <async-fragment data-provider="data.adsProvider"
                var="ads">
                <!-- Do something with the ads data... -->
            </async-fragment>

        </section>
    </div>
    <footer>
        <!-- Footer -->
    </footer>
    <!-- Body <script> tags -->
</body>
</html>

在该例子中,"Search results"的异步碎片将会作为HTML的模板中的第一个元素显示,但是它确实加载完成时间所需最久的. 所以所有随后的碎片将会需要被缓存在服务器端. 这个结果的瀑布模型图如下所示:

这种方式的性能可能还不错,接下来我们会在下一章节讲述无序(out-of-order)的加载方式以及它的性能.

异步碎片的无序刷新(out-of-order)

Marko通过如下方式实现了无序的碎片刷新:

为了取消异步碎片的加载等待, 在输出的流中已经被写入了一个拥有唯一指派ID的HTML占位符. 无序的异步碎片在<body>结束前被渲染. 每一个无序的异步碎片被渲染在一个隐藏的<div>标签中. 之后,一个<script>代码将会替换掉DOM中对应的无序碎片. 当所有的无序异步碎片完成时,HTML流将会被刷新并结束相应请求.

这里举例说明无序刷新的代码如下:

<html>
<head>
    <title>Clothing Store</title>
    <!-- 1a) Head <link> tags -->
</head>
<body>
    <header>
        <!-- 1b) Header -->
    </header>
    <div class="body">
        <main>
            <!-- 2) Search Results -->
            <span id="asyncFragment0Placeholder"></span>
        </main>
        <section class="filters">
            <!-- 3) Search filters -->
            <span id="asyncFragment1Placeholder"></span>
        </section>
        <section class="ads">
            <!-- 4) Ads -->
            <span id="asyncFragment2Placeholder"></span>
        </section>
    </div>
    <footer>
        <!-- 5a) Footer -->
    </footer>

    <!-- 5b) Body <script> tags -->

    <script>
    window.$af=function(){
    // Small amount of code to support rearranging DOM nodes
    // Unminified:
    // https://github.com/raptorjs/marko-async/blob/master/client-reorder-runtime.js
    };
    </script>

    <div id="asyncFragment1" style="display:none">
        <!-- 4) Ads content -->
    </div>
    <script>$af(1)</script>

    <div id="asyncFragment2" style="display:none">
        <!-- 3) Search filters content -->
    </div>
    <script>$af(2)</script>

    <div id="asyncFragment0" style="display:none">
        <!-- 2) Search results content -->
    </div>
    <script>$af(0)</script>

</body>
</html>

值得注意的是无序刷新技术需要客户端的运行环境支持Javascript,这样才能把每一个无序碎片最后在DOM中组装起来. 因此,你应该在客户端支持Javascript的时候才启用无序碎片模式,并且,由于移动DOM可能会引起你的页面重新布局,这样会一定程度的绕软用户的视觉,还会造成客户端更多的CPU消耗. 如果说重新布局感觉不好,Marko也提供了替换的内容应用于等待异步碎片的时候.

如果要在Marko中启用无序异步碎片,请在标签<async-fragment>中添加属性client-reorder=true, 并且<async-fragments>标签必须被加入到页面的最底部, 它将会作为渲染无序碎片的容器. 下面是更新了<async-fragment>标签的代码:

<async-fragment data-provider="data.searchResultsProvider"
    var="searchResults"
    client-reorder="true>
    ...
</async-fragment>

更新后的页面包含<async-fragments>如下:

<html>
<head>
    <title>Clothing Store</title>
    <!-- Head <link> tags >
</head>
<body>
    ...

    <!-- Body <script> tags -->

    <async-fragments/>
</body>
</html>

为了更好的与无序碎片结合使用,最好的方式是把<script>标签放在尽可能最先被刷新的地方(在所有无序碎片之前). 这样当服务器在准备呈献数据时,客户端就已经能够下载页面所需的Javascript, 这样一来,用户就能更早的与页面交互了.

最终的无序碎片加载的瀑布模型图如下所示:

最终的瀑布模型图显示出的加载策略可以看出无序碎片的异步加载能够最有效的提高页面的加载时间和用户的实际体验.

  • cwzhsi

    异步碎片的无序刷新时,为什么需要用一个隐藏的div呢?

    #1
  • 一米

    @cwzhsi 因为对于 html 来说,已经发送到了浏览器了,
    例如:这句 html‘

    主要区域


    已经发送给浏览器了,这时候后端没办法再改变这里了,所以只能留隐藏的 div 并且后面真正的数据刷新出来之后,才通过 js 来更新这里的内容。

    @leonli 有没有测试过?对于增加的页面大小,节约的这么点时间,应该有一个怎样的平衡?或者说,更好的选择是调整 DOM 顺序而不是异步碎片无序刷新。

    #2
登录后回复,如无账号,请使用邀请码注册