WebKit Page Cache II - unload 事件

之前我简单介绍了 Page Cache 的具体作用,并概述了我们正在改进的一些方面。

这篇博文面向 Web 开发者,因此比上一篇更加技术性。

在本文中,我想更深入地讨论 unload 事件处理器,为什么它们会阻止页面进入 Page Cache,以及如何改进现状。

Load/Unload 事件处理器

Web 开发者可以利用 load 和 unload 事件在网页生命周期的特定时刻执行工作。

load 事件的目的是相当直接的:在新页面加载完成后执行初始设置。

unload 事件则相对神秘。每当用户离开页面时,页面就会被“卸载(unloaded)”,脚本可以执行一些最后的清理工作。

神秘之处在于,“离开页面”可能意味着几种不同的情况:

  1. 用户关闭浏览器标签页或窗口,导致可见页面被销毁。
  2. 浏览器从旧页面导航到新页面,导致旧的可见页面被销毁。

Page Cache 通过增加一种新的导航可能性,使得事情变得更加有趣:

  1. 浏览器从旧页面导航到新页面,但旧的可见页面被暂停、隐藏并放入 Page Cache 中。

现状

unload 事件处理器的目的是在可见页面即将被销毁时执行一些最后的清理工作。但是,如果页面进入 Page Cache,它就会被暂停、隐藏,并且不会立即被拆毁。这就带来了一些有趣的复杂性。

如果在页面进入 Page Cache 时触发 unload 事件,那么处理器可能会执行破坏性操作,导致用户返回时页面变得无法使用。

如果每次离开页面时都触发 unload 事件,包括每次进入 Page Cache 时以及最终被销毁时,那么处理器可能会多次执行只需执行一次的关键性工作。

如果在页面进入 Page Cache 时不触发 unload 事件,那么我们将面临一种可能性:页面在暂停和隐藏状态下被销毁,而 unload 处理器可能永远不会运行。

如果在页面进入 Page Cache 时不触发 unload 事件,但考虑在暂停的页面最终被销毁时触发它,那么我们正在考虑做一件以前从未做过的事情:执行属于一个已按下“暂停”按钮的不可见网页的脚本。

要使这一切顺利运行,存在各种障碍,包括技术难题、安全问题和用户体验方面的考量。

由于没有明确的解决方案来处理此类页面,所有主要的浏览器厂商都达成了相同的结论:不缓存这些页面。

您可以如何帮助

Web 开发者可以做一些事情来帮助他们的页面变得可缓存。

一种方法是仅在代码与当前浏览器相关时安装 unload 事件处理器。例如,我们见过类似于以下代码的 unload 处理器:

    function unloadHandler()
    {
        if (_scriptSettings.browser.isIE) {
            // Run some unload code for Internet Explorer
            ...
        }
    }

在除 Internet Explorer 之外的所有浏览器中,这段代码什么都没做,但它的存在本身就可能减慢用户的体验。这位开发者本应该在安装 unload 处理器*之前*进行浏览器检查。

开发者改进事情的另一种方法是仅在页面需要监听 unload 事件时安装处理器,并在不再需要时将其移除。

例如,用户可能正在处理文档草稿,开发者因此安装了一个 unload 处理器,以确保在离开页面前保存草稿。但他们也可能启动了一个计时器,每分钟左右自动保存一次。如果计时器触发,文档草稿已保存,并且用户没有进行进一步更改,那么 unload 处理器就应该被移除。

特别精明的开发者可能会考虑第三个选项。

unload 事件的替代方案

一段时间前,Mozilla 通过发明 load/unload 事件的替代方案,以不同的方式解决了这个问题。

load 和 unload 事件的本意是只触发一次,这是问题的根本原因。而 pageshow/pagehide 事件——我们已在 修订版本 47824 中将其实现到 WebKit 中——则解决了这个问题。

尽管名字如此,pageshow/pagehide 事件与页面是否实际显示在屏幕上无关。例如,当你最小化窗口或切换标签页时,它们不会触发。

它们的作用是增强 load/unload 事件,使其在涉及导航的更多情况下工作。考虑 load/unload 事件处理器可能如何使用的示例:

    <html>
    <head>
    <script>

    function pageLoaded()
    {
        alert("load event handler called.");
    }

    function pageUnloaded()
    {
        alert("unload event handler called.");
    }

    window.addEventListener("load", pageLoaded, false);
    window.addEventListener("unload", pageUnloaded, false);

    </script>
    <body>
    <a href="http://www.webkit.org/">Click for WebKit</a>
    </body>
    </html>

点击此处在新窗口中查看此示例,以防你猜不到它的作用。

尝试点击链接离开页面,然后按下后退按钮。相当直接。

pageshow/pagehide 事件在 load/unload 触发时也会触发,但它们还有一个额外的技巧。

pageshow 事件不仅在页面“加载”的单一离散时刻触发,而且在页面从 Page Cache 恢复时也会触发。

类似地,pagehide 事件在 unload 事件触发时也会触发,并且在页面被暂停进入 Page Cache 时也会触发。

通过在事件中包含一个名为“persisted”的额外属性,事件可以告诉页面它们代表的是 load/unload 事件,还是从 Page Cache 保存/恢复的操作。

这是使用 pageshow/pagehide 的相同示例:

    <html>
    <head>
    <script>

    function pageShown(evt)
    {
        if (evt.persisted)
            alert("pageshow event handler called.  The page was just restored from the Page Cache.");
        else
            alert("pageshow event handler called for the initial load.  This is the same as the load event.");
    }

    function pageHidden(evt)
    {
        if (evt.persisted)
            alert("pagehide event handler called.  The page was suspended and placed into the Page Cache.");
        else
            alert("pagehide event handler called for page destruction.  This is the same as the unload event.");
    }

    window.addEventListener("pageshow", pageShown, false);
    window.addEventListener("pagehide", pageHidden, false);

    </script>
    <body>
    <a href="http://www.webkit.org/">Click for WebKit</a>
    </body>
    </html>

点击此处在新窗口中查看此示例,但请确保你使用的是最新的 WebKit nightly 版本

请记住尝试点击链接离开页面,然后按下后退按钮。

挺酷的吧?

这些新事件实现了什么

pagehide 事件很重要,原因有二:

  1. 它使 Web 开发者能够区分页面被暂停和被销毁的情况。
  2. 当它被用来替代 unload 事件时,它使得浏览器能够使用它们的 Page Cache。

更改现有代码以使用 pagehide 而非 unload 也相当直接。这是一个测试 onpageshow 属性的示例,以便在支持时选择 pageshow/pagehide,不支持时回退到 load/unload:

    <html>
    <head>
    <script>

    function myLoadHandler(evt)
    {
        if (evt.persisted) {
            // This is actually a pageshow event and the page is coming out of the Page Cache.
            // Make sure to not perform the "one-time work" that we'd normally do in the onload handler.
            ...
            
            return;
        }
    
        // This is either a load event for older browsers, 
        // or a pageshow event for the initial load in supported browsers.
        // It's safe to do everything my old load event handler did here.
        ...
    }

    function myUnloadHandler(evt)
    {
        if (evt.persisted) {
            // This is actually a pagehide event and the page is going into the Page Cache.
            // Make sure that we don't do any destructive work, or work that shouldn't be duplicated.
            ...
            
            return;
        }
    
        // This is either an unload event for older browsers, 
        // or a pagehide event for page tear-down in supported browsers.
        // It's safe to do everything my old unload event handler did here.
        ...
    }

    if ("onpagehide" in window) {
        window.addEventListener("pageshow", myLoadHandler, false);
        window.addEventListener("pagehide", myUnloadHandler, false);
    } else {
        window.addEventListener("load", myLoadHandler, false);
        window.addEventListener("unload", myUnloadHandler, false);
    }

    </script>
    <body>
    Your content goes here!
    </body>
    </html>

小菜一碟!

您可以如何帮助:重温

重申一下,我们现在确定了 Web 开发者可以帮助 Page Cache 更好地工作的三个好方法:

  1. 仅在代码与当前浏览器相关时安装事件处理器。
  2. 仅在页面实际需要时安装事件处理器。
  3. 如果浏览器支持,请使用 pagehide 代替。

故意忽视这些选项中的任何一个或所有这些选项的 Web 开发者主要只实现了一件事:
迫使用户进入“慢速导航模式”。

我作为一个浏览器工程师和浏览器用户都想说:这太糟糕了!

情况变得复杂

但是,既然我们已经介绍了精明和有礼貌的 Web 开发者未来可以做什么来帮助,我们需要进一步审视 Web 的当前状态。

浏览器将 unload 处理器视为神圣的,因为它旨在执行“重要工作”。不幸的是,许多热门网站的 unload 事件处理器明显*没有*执行“重要工作”。我经常看到以下类型的处理器:

  • 总是更新一些用于跟踪的 cookie,即使它已经更新过了。
  • 总是向服务器发送草稿数据的 XHR 更新,即使它已经发送过了。
  • 什么都没做,无法在未来的任何浏览会话中持续存在。
  • 是空的。它们实际上*什么*都没做。

由于这些行为不端的页面非常普遍,并将导致 WebKit 的 Page Cache 改进效果不佳,我们中的一些人开始提出问题:

如果我们只是开始将这些页面加入 Page Cache,而不先运行 unload 事件处理器,实际*会*发生什么?

会破坏什么?

我们能否检测出某种模式来确定 unload 事件处理器是否“重要”?

我们的实验

不尝试,就永远不知道。

修订版本 48388 开始,我们允许带有 unload 处理器的页面进入 Page Cache。如果用户在页面可见时关闭窗口,unload 事件将照常触发。但当用户离开页面时,unload 事件将不会像通常那样触发。如果用户在页面处于暂停状态并位于 Page Cache 中时关闭窗口,unload 事件处理器将永远不会运行。

这对用户意味着在常见情况下,他们的导航体验会明显更流畅、更快速。这对开发者意味着我们有意决定不运行他们的一些代码,他们的 Web 应用程序可能会因此损坏。

对于用户和开发者来说——请在跟踪此实验的错误报告中留下您的反馈、观察或建议。

请记住,这只是一个实验。没有人计划在生产产品中推出这一剧烈的行为改变。但 Page Cache 是浏览器性能如此重要的一部分,我们愿意稍微突破界限,以便大幅改进它。

我们想知道什么会损坏。我们想知道我们是否能启发式地确定 unload 处理器是否真正关键。我们想知道是否能检测到某些类型的 unload 处理器中的特定模式并区别对待它们。而且,也许最重要的是,我们想进行宣传。

至少有一个流行的 Javascript 库已经采纳了我们给出的一些建议,以帮助改善 Web 生态。如果仅仅是更多热门网站或库的开发者注意到了这个实验并改变他们的代码,那么 Web 将会是所有人更友好的地方。