Surfin’ Safari

WebKit Page Cache II – The unload Event

Posted by Brady Eidson on Monday, September 21st, 2009 at 4:29 pm

Previously I touched on what exactly the Page Cache does and outlined some of the improvements we’re working on.

This post is geared towards web developers and is therefore even more technical than the last.

In this article I’d like to talk more about unload event handlers, why they prevent pages from going into the Page Cache, and what can be done to make things better.

Load/Unload Event Handlers

Web developers can make use of the load and unload events to do work at certain points in the lifetime of a web page.

The purpose of the load event is quite straightforward: To perform initial setup of a new page once it has loaded.

The unload event is comparatively mysterious. Whenever the user leaves a page it is “unloaded” and scripts can do some final cleanup.

The mysterious part is that “leaving the page” can mean one of a few things:

  1. The user closes the browser tab or window, resulting in the destruction of the visible page.
  2. The browser navigates from the old page to a new page, resulting in the destruction of the old visible page.

The Page Cache makes this even more interesting by adding a new navigation possibility:

  1. The browser navigates from the old page to a new page, but the old visible page is suspended, hidden, and placed in the Page Cache.

The Status Quo

Unload event handlers are meant to do some final cleanup when the visible page is about to be destroyed. But if the page goes into the Page Cache it becomes suspended, is hidden, and is not immediately torn down. This brings up interesting complications.

If we fire the unload event when going into the Page Cache, then the handler might be destructive and render the page useless when the user returns.

If we fire the unload event every time a page is left, including each time it goes into the Page Cache and when it is eventually destroyed, then the handler might do important work multiple times that it was critical to only do once.

If we don’t fire the unload event when going into the Page Cache, then we face the possibility that the page will be destroyed while it is suspended and hidden, and the unload handler might never be run.

If we don’t fire the unload event when going into the Page Cache but consider firing it whenever the suspended page is eventually destroyed, then we’re considering the possibility of doing something that’s never been done before: Executing scripts that belong to an invisible web page that has had its “pause” button pressed.

There’s all sorts of obstacles in making this work well including technological hurdles, security concerns, and user-experience considerations.

Since there is no clear solution for handling such pages the major browsers vendors have all come to the same conclusion: Don’t cache these pages.

How You Can Help

Web developers have a few things they can do to help their pages be cacheable.

One is to only install the unload event handler if the code is relevant to the current browser. For example, we’ve seen unload handlers similar to the following:

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

In all browsers other than Internet Explorer this code does nothing, but its mere existence potentially slows down their user experience. This developer should’ve done the browser check *before* installing the unload handler.

Another way developers can improve things is to only install the unload event handler when the page has a need to listen for it, then remove it once that reason has passed.

For example the user might be working on a draft of a document so the developer installs an unload handler to make sure the draft gets saved before the page is left. But they also start a timer to automatically save it every minute or so. If the timer fires, the document draft is saved, and the user doesn’t make any further changes, the unload handler should be removed.

Particularly savvy developers might consider a third option.

A Replacement For Unload

Some time ago Mozilla approached this problem differently by inventing a replacement for load/unload events.

The load and unload events are meant to be fired exactly once, and this is the underlying cause of the problem. The pageshow/pagehide events – which we’ve implemented in WebKit as of revision 47824 – address this.

Despite their name the pageshow/pagehide events don’t have anything to do with whether or not the page is actually visible on the screen. They won’t fire when you minimize the window or switch tabs, for example.

What they do is augment load/unload to work in more situations involving navigation. Consider this example of how load/unload event handlers might be used:

    <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>

Click here to view this example in a new window, in case you can’t guess what it does.

Try clicking the link to leave the page then press the back button. Pretty straightforward.

The pageshow/pagehide fire when load/unload do, but also have one more trick up their sleeve.

Instead of firing only at the single discrete moment when a page is “loaded” the pageshow event is also fired when pages are restored from the Page Cache.

Similarly the pagehide event fires when the unload event fires but also when a page is suspended into the Page Cache.

By including an additional property on the event called “persisted” the events tell the page whether they represent the load/unload events or saving/restoring from the Page Cache.

Here’s the same example using 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>

Click here to view this example in a new window, but make sure you’re using a recent WebKit nightly.

Remember to try clicking the link to leave the page then press the back button.

Pretty cool, right?

What These New Events Accomplish

The pagehide event is important for two reasons:

  1. It enables web developers to distinguish between a page being suspended and one that is being destroyed.
  2. When used instead of the unload event, it enables browsers to use their page cache.

It’s also straightforward to change existing code to use pagehide instead of unload. Here is an example of testing for the onpageshow attribute to choose pageshow/pagehide when supported, falling back to load/unload when they’re not:

    <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>

Piece of cake!

How You Can Help: Revisited

To reiterate, we’ve now identified three great ways web developers can help the Page Cache work better:

  1. Only install the event handler if the code is relevant to the current browser.
  2. Only install the event handler once your page actually needs it.
  3. If supported by the browser, use pagehide instead.

Web developers that willfully ignore any or all these options are primarily accomplishing one thing:
Forcing their users into “slow navigation mode.”

I say this both as a browser engineer and a browser user: That stinks!

The Plot Thickens

But now that we’ve covered what savvy and polite web developers can do to help in the future, we need to further scrutinize the current state of the web.

Browsers treat the unload handler as sacred because it is designed to do “important work.” Unfortunately many popular sites have unload event handlers that decidedly do not “do important work.” I commonly see handlers that:

  • Always update some cookie for tracking, even though it’s already been updated.
  • Always send an XHR update of draft data to a server, even though it’s already been sent.
  • Do nothing that could possible persist to any future browsing session.
  • That are empty. They literally do nothing.

Since these misbehaved pages are very common and will render improvements to WebKit’s Page Cache ineffective a few of us started to ask the question:

What *would* actually happen if we simply started admitting these pages to the Page Cache without running the unload event handler first?

What would break?

Can we detect any patterns to determine whether an unload event handler is “important” or not?

Our Experiment

You never know for sure until you try.

Starting in revision 48388 we’ve allowed pages with unload handlers into the Page Cache. If a user closes the window while the page is visible, the unload event will fire as usual. But the unload event will not be fired as normal when the user navigates away from the page. If the user closes the window while the page is suspended and in the Page Cache, the unload event handler will never be run.

What this means for users is that their navigation experience could be noticeably smoother and quicker in the common case. What this means for developers is that we’re consciously deciding not to run some of their code and their web application might break.

For users and developers alike – Please leave your feedback, observations, or suggestions in the bug tracking this experiment.

And remember this is just an experiment. No one is planning to ship this drastic change in behavior in a production product. But the Page Cache is such an important part of browser performance that we’re willing to push the envelope a little to improve it a lot.

We want to learn what breaks. We want to know if we can heuristically determine if an unload handler is truly critical or not. We want to know if we can detect certain patterns in some types of unload handlers and treat them differently. And, perhaps most importantly, we want to evangelize.

At least one popular Javascript library has already adopted some of the advice we’ve given to help improve the landscape on the web. If just a few more developers for popular sites or libraries take notice of this experiment and change their code then the web will be a much friendlier place for all of us.

WebKit Page Cache I – The Basics

Posted by Brady Eidson on Wednesday, September 16th, 2009 at 4:47 pm

This is the first of two posts that will center around a modern browser engine feature that doesn’t usually get a lot of press: The Page Cache.

Today I’ll talk a bit about what this feature is, why it often doesn’t work, and what plans we have to improve it.

Page Cache Overview

Some of you might be more familiar with what other browsers call their Page Cache. Firefox calls theirs the “Back-Forward Cache” or “bfcache.” Opera refers to theirs as “Fast History Navigation.” We’ve recently started to refer to WebKit’s implementation as the “Page Cache” to reduce confusion with our “Back/Forward List.”

Note that the Page Cache is an end user feature that makes navigating the web much smoother. It is not a “cache” in the “HTTP sense“. It is not a “cache” in the “disk cache” sense where raw resources are stored on the local disk. And it’s not a “cache” in the traditional “memory cache” sense where WebKit keeps decoded resources around in memory to be shared between multiple web pages.

So… what *exactly* is it?

Quite simply, the Page Cache makes it so when you leave a page we “pause” it and when you come back we press “play.”

When a user clicks a link to navigate to a new page the previous page is often thrown out completely. The DOM is destroyed, Javascript objects are garbage collected, plug-ins are torn down, decoded image data is thrown out, and all sorts of other cleanup occurs.

When this happens and the user later clicks the back button it can be painful for them. WebKit may have to re-download the resources over the network, re-parse the main HTML file, re-run the scripts that dynamically setup the page, re-decode image data, re-layout the page, re-scroll to the right position, and re-paint the screen. All of this work requires time, CPU usage, and battery power.

Ideally the previous page can instead be placed in the Page Cache. The entire live page is kept in memory even though it is not on screen. This means that all the different bits and pieces that represent what you see on the screen and how you interact with it are suspended instead of destroyed. They can then be revived later in case you click the back button.

Why is This Important?

When the Page Cache works it makes clicking the back button almost instantaneous.

You can do a search, click a search result, then go back and immediately be looking at the exact same results page. You might be browsing an aggregator site like Reddit or Digg and want to rapidly view a lot of different links in the same tab. You might be navigating an image gallery and decide to compare two images by alternately clicking “back” and “forward” rapidly. Or you might have simply clicked on the wrong link and want to go back to correct your mistake.

Anytime you might click the back button or the forward button you unknowingly hope the Page Cache is on your side. When the Page Cache is used, users are happy even though they’re not aware of the magic behind the scenes.

Conversely, when the Page Cache is bypassed, users often get frustrated with both the browser and the Web in general.

Why Wouldn’t it Work?

So if the Page Cache is so amazing, why doesn’t WebKit always use it when you navigate to a new page?

There’s a few main answers to that question.

Some Pages aren’t Interesting

First off, sometimes it doesn’t make sense to cache a page because it’s not interesting to return to in the exact same state. For example, the page might not even be finished loading yet. Or the page might’ve had an error loading. Or maybe the page was a redirection page that exists solely to automatically move the user to some new URL.

These are cases where we’re happy with the current Page Cache behavior in WebKit.

Some Pages are Complicated

Secondly, a page might not be considered for the Page Cache because it’s difficult to figure out how to “pause” it. This happens with more complex pages that do interesting things.

For example, plug-ins contain native code that can do just about anything it wants so WebKit can’t “hit the pause button” on them. Another example is pages with multiple frames which WebKit has historically not cached.

Distressingly, navigating around these more advanced pages would benefit the most from the Page Cache.

Some Pages are Secure

Server administrators for HTTPS sites often have particular security concerns and are very sensitive with regards to how browsers behave. For example, Financial institutions are often very thorough in verifying each particular browser’s behavior before allowing it to be used by their customers.

One area often focused on is back/forward behavior. Such institutions are – understandably – very picky about the types of data left behind in the browser as a user navigates. As a result, in an effort to err on the side of extreme caution, WebKit has disallowed all HTTPS sites from its Page Cache since the very beginning.

A more fine grained approach might go a long way towards improving the user experience.

Planned Improvements

Clearly there’s some important cases we don’t handle and therefore plenty of room for improvement.

WebKit’s Page Cache was originally written in 2002 before the very first Safari beta release. Its capabilities reflected both the architecture of WebKit at the time and the landscape of the Web in 2002.

The Web of 2009 is a much different place and we need to bring the Page Cache up to par. Fortunately this work is well underway.

For example, as of revision 48036 a major limitation was resolved and pages with frames are now placed in the Page Cache. Browsing with the latest WebKit nightly always seems to “feel faster” in ways you can’t quite put your finger on, and recently some of you might have been experiencing this enhancement.

But there’s plenty more work to do.

Plug-ins are the next huge one on our hit list. As I mentioned earlier, plug-ins can run whatever native code they like so we can’t reliably hit the “pause” button on them.

Earlier versions of WebKit handled single-frame pages with some types of plug-ins. WebKit would tear down the plug-in when leaving the page and restoring it when the user returned. But as work continued on WebCore to make it faster and easier to port, this ability was lost.

Bug #13634 tracks getting this working again for all plug-ins on all pages.

Then there are HTTPS pages. We completely ban them now, but a more selective approach should be able to benefit users as well as keep security-minded institutions happy.

Bug #26777 tracks allowing HTTPS pages to be cached unless their response headers include “cache-control: no-store” or “cache-control: no-cache” which has become the canonical way for a selective organization to secure your content.

If you have any other ideas for what else might be improved, please feel free to comment in the appropriate bug or file a new bug of your own!

Unload Handlers

One thing I haven’t mentioned is pages with unload event handlers.

The unload event was designed to let a page do some cleanup work when the user closes the page.

The browser can’t fire the unload event before it puts the page in the Page Cache, because the page then assumes it is in a terminal state and might destroy critical parts of itself. This completely defeats the purpose of the Page Cache.

But if the browser puts the page in the Page Cache without running the unload handler, then the page might be destroyed by the browser while it is “paused” and hidden, and that cleanup work – which might be very important – will never happen.

Since the unload event’s purpose is to allow “important work when a page is closed,” all major browsers refuse to put such pages in their Page Cache, causing a direct negative impact on the user experience.

In a future post I’ll be talking more about unload event handlers and there will actually be homework for many of you web developers out there! Stay tuned…

Eric Carlson is now a WebKit Reviewer

Posted by Eric Seidel on Tuesday, September 1st, 2009 at 3:34 am

Eric Carlson has been a long-time contributer to WebKit, first through the QuickTime Plugin, and now directly as part of WebKit’s HTML5 media support. Eric knows a ridiculous amount about video and audio and we’re very glad to have him now as an official reviewer! Please join me in welcoming Eric as a reviewer.

3D Transforms

Posted by Simon Fraser on Thursday, July 16th, 2009 at 12:02 pm

WebKit on Mac OS X now has support for CSS 3D transforms, which allow you to position elements on the page in three-dimensional space using CSS. This is a natural extension of 2D transforms, which we described in an earlier blog post. 3D transforms have been supported on iPhone since 2.0, and now we’re please to announce that we have currently added support for Leopard and later.

If you want to jump right in and see a demo, make sure you’re running recent WebKit nightly build on Leopard or later, and load this example:

Poster Circle

Here’s a screenshot for those not running a recent-enough WebKit (if you are, hover over it for a treat!):

Like many of the examples you’ll see here, this one combines CSS transforms with CSS transitions and animations to great effect.

3D transforms are applied via the same -webkit-transform property as 2D transforms. For example, here’s how to rotate an element about the Y (vertical) axis:

-webkit-transform: rotateY(45deg);

There are several new transform functions available for use in the -webkit-transform property:

translate3d(x, y, z), translateZ(z)
Move the element in x, y and z, and just move the element in z. Positive z is towards the viewer. Unlike x and y, the z value cannot be a percentage.
scale3d(sx, sy, sz), scaleZ(sz)
Scale the element in x, y and z. The z scale affects the scaling along the z axis in transformed children.
rotateX(angle), rotateY(angle), rotate3d(x, y, z, angle),
The first two forms simply rotate the element about the horizontal and vertical axes. Angle units can be degrees (deg) radians (rad) or gradians (grad). The last form allows you to rotate the element around an arbitrary vector in 3D space; x, y and z should specify the unit vector you wish to rotate around (we’ll normalize it for you).
perspective(p)
This function allows you to put some perspective into the transformation matrix. For an explanation of p, see below. Normally perspective is applied via the -webkit-perspective property, but this function allows you to get a perspective effect for a single element, with something like:

-webkit-transform: perspective(500px) rotateY(20deg);
matrix3d(…)
This function allows you to specify the raw 4×4 homogeneous transformation matrix of 16 values in column-major order. Have fun with that!

We’ve also extended one other CSS transform property, and implemented the four other 3D-related properties described in the spec:

-webkit-transform-origin now accepts three values, allowing you to specify a z offset for the transform origin.

-webkit-perspective is used to give an illusion of depth; it determines how things change size based on their z-offset from the z=0 plane. You can think of it as though you’re looking at the page from a distance p away. Objects on the z=0 plane appear in their normal size. Something at a z offset of p/2 (halfway between the viewer and the z=0 plane) will look twice as big, and something at a z offset of -p will look half as big. Thus, large values give a little foreshortening effect, and small values lots of foreshortening. Values between 500px and 1000px give a reasonable-looking result for most content.

The default origin for the perspective effect is the center of the element’s border box, but you can control this with -webkit-perspective-origin.

Here’s an example that shows how perspective works:

Perspective

The interesting thing about -webkit-perspective is that it does not affect the element directly. Instead, it affects the appearance of the 3D transforms on the transformed descendants of that element; you can think of it as adding a transform that gets multiplied into the descendant transforms. This allows those descendants to all share the same perspective as they move around.

We’ve described how you can assign 3D transforms to elements and make them look three-dimensional with some perspective. However, so far, all the effects are really just painting effects. Those transformed children are still rendering into the plane of their parent; in other words, they are flattened.

When you start to build hierarchies of objects with 3D transforms, flattening is not what you want. You want parents and children to live in a shared three-dimensional space, and to all share the same perspective which propagates up from some container. This is where -webkit-transform-style comes in.

-webkit-transform-style has two values:

  • flat: This is the default value, and gives the behavior described above; transformed children are flattened into the plane of their parent (think of the 3D transform as simply a painting effect).
  • preserves-3d: This value states that the element to which it is assigned does not flatten its children into it; instead, those children live in a shared 3D space with the element.

Here’s an example that shows transform-style in action:

Transform Style

A common pattern, therefore, is to have content that looks like this:

<div class="container" style="-webkit-perspective: 600px">
  <div class="box" style="-webkit-transform-style: preserve-3d; -webkit-transform: rotateY(10deg)">
    <div class="leaf" style="-webkit-transform: rotateX(10deg)"></div>
  </div>
</div>

Here both ‘leaf’ and ‘box’ share the same 3D space, so both appear with the perspective specified on the container. ‘box’ can also be rotated with a transition or animation, and ‘leaf’ will move around as ‘box’ moves, in perspective.

One thing you may have noticed in these demos is that it’s quite common to have a 3D transform that flips an element around so that you can see its reverse side. In some cases you don’t want the element to appear at all in this situation (say, for example, you want to position two elements back-to-back, so you need to hide the one that’s facing away from the viewer). This is the reason for the last 3d-related property, -webkit-backface-visibility. Its two values—visible (the default), and hidden—specify whether the element is visible or not when that element is transformed such that its back face is towards the viewer.

Here’s a final example that shows backface-visibility in action, along with more 3D goodness, animations and transitions:

Morphing Power Cubes

Mighty Cubes

For more information, see the CSS working drafts on 2D transforms, 3D transforms, transitions and animations. There is also documentation in the Safari Reference Library.

We hope you have a blast with these new features, and share your creations with us. If you find bugs, please report them at bugs.webkit.org.

Adam Barth and Dave Levin are now WebKit reviewers

Posted by Eric Seidel on Monday, June 8th, 2009 at 3:13 pm

Adam has been a long-time contributer of security fixes. Dave comes to us through the Chromium porting effort and has done quite a bit of work on HTML5 Workers as well. Please join me in congratulating Adam and Dave on their reviewer status!