Surfin’ Safari

Web Inspector Updates

Posted by Joseph Pecoraro on Tuesday, November 3rd, 2009 at 7:13 pm

A number of exciting new features have been added to the Web Inspector since our last update. Today we would like to highlight some of those features! This post is also available in Japanese (日本語), thanks to Keishi Hattori (服部慶士).

If you would like to play with most of these features you will need to be running a recent WebKit Nightly. Once downloaded make sure that you enable the Web Inspector by checking “Show Develop menu in menu bar” under the Advanced tab in the Preferences.

Enabling the Inspector in Preferences

Editing Element Attributes and Style Properties #

Editing Element Attributes and Style Properties has been made even simpler and more developer friendly. The interfaces for editing attributes and properties now support tabbing to allow you to move between items, and create new items with ease.

Besides tabbing you can also add a new attributes to a node.  Start by hovering over the node in the Element’s Tree Hierarchy and after a polite wait a clickable template for a new attribute will appear.

Edit Element Attributes

Related Bug Reports: Add Element Attributes, Tabbing, and Improved UI.

Creating and Modifying CSS Rules and Selectors #

A powerful new feature in the Web Inspector allows you create new or modify existing CSS Rules and Selectors. We expect both developers and designers will find this very useful when experimenting with new ideas or tweaking existing designs.

The interface for working with selectors starts with a new Gear Menu in the Styles Sidebar Pane. Select “New Style Rule” and a new section will be created for you, pre-populated with an intelligent selector from the selection in the Elements Tree Hierarchy. Editing selectors is activated by double-clicking. Once again, tabbing will allow you to navigate between selectors and their properties.

When editing selectors there is visual feedback when you create or modify a selector that does not affect the selected node in the Elements Tree Hierarchy. This indicator helps detect errors when making changes.

Create and Modify CSS Selectors

One more tweak to the Styles Pane is that there is always a section for the selected node’s style attribute. This allows you to easily add style information to the node as you usually would via the Styles Pane instead of editing or creating a “style” attribute. This section is nearly always on top due to how CSS specificity works.

Related Bug Reports: Selectors Support and Move to Gear Menu.

CSS Color Representations #

Colors in the Styles Pane can be shown in any of their possible representations. For simple colors this includes short hex, full hex, rgb, hsl, and potentially a nickname. For advanced colors this includes rgba and hsla. For example the color “white” can be represented as: #FFF, #FFFFFF, rgb(255, 255, 255), hsl(0, 100%, 100%), and white.

You can use the Styles Pane’s Gear Menu to set your preferred representation. However, if you want to cycle through an individual color’s different representations you can do so by clicking on its associated color swatch.

Mutiple Color Representations

Related Bug Reports: Color Representations, Preference and Gear Menu, and UI Improvement.

DOM Storage #

The Storage Panel (formerly the Databases Panel) now allows you to monitor DOM Storage areas like localStorage and sessionStorage in a familiar datagrid. The DOM Storage datagrid displays live updates so monitoring changes is possible without manually refreshing the view.

Also, the familiar creation and editing techniques apply to the datagrid. To add a new key/value pair just double-click in any open area, or double-click an existing item to start editing. Tabbing works as you would expect.

Observing DOM Storage Key/Value Pairs

Related Bug Reports: DOM Storage Support, Live Updates, Create New Items, and Tabbing.

Keyboard Shortcuts #

Keyboard shortcuts are always desired by developers. They can be hard to discover, so here is a complete list and here are the ones that were added recently:

  • Switch PanelsCommand-[ and Command-] on a Mac or Control-[ and Control-] on other platforms.
  • Delete a Node in the Tree Hierarchy — either Delete or Backspace keys will do the trick.
  • Quick Edits in the Tree Hierarchy — Hitting Enter or Return on a Node in the Tree enters the editing mode for that type of Node. For a Text Node you will start editing the content. For Element Nodes you start editing the first attribute, or, for convenience, a new attribute will be added for you.

The Scripts Debugger was updated to support some popular keyboard shortcuts:

  • ContinueF8 or Command-/ on a Mac or Control-/ on other platforms.
  • Step OverF10 or Command-’ on a Mac or Control-’ on other platforms.
  • Step IntoF11 or Command-; on a Mac or Control-; on other platforms.
  • Step OutShift-F11 or Shift-Command-; on a Mac or Shift-Control-; on other platforms.
  • Next Call FrameControl-. on all platforms.
  • Previous Call FrameControl-, on all platforms.
  • Evaluate Selection When on a BreakpointShift-Command-E on a Mac or Shift-Control-E on other platforms.

Related Bug Reports: Switch PanelsDelete Node, Quick Edit, General Debugger Shortcuts and Evaluate Selection.

Cookies #

Viewing Cookie information is now possible under the Storage Panel. Supported platforms show all of the cookies and their hidden information for all domains accessed on the inspected page. Cookie information includes the name, value, path, expiration date, http only flag, and secure (https) flag. Supported platforms may also delete cookies.

If your platform doesn’t have full support you aren’t left in the dark. You will still be able to see the keys and values of the cookies that are accessible via JavaScript on the inspected page.

Inspect Hidden Cookie Information

Related Bug Reports: Initial Support, Hidden Data 1 and 2, Cookies for Sub-Resources, and UI Improvements

Event Listeners #

A new Sidebar Pane has been added to the Elements Panel which displays the registered Event Listeners for the selected node.  The Event Listeners that are shown for the selected node are in the exact order that they are fired through the Capturing and Bubbling phases.  This provides developers with the most accurate and useful information possible.

The user interface shows the registered Event Listeners separated by type. If a node has both “onclick” and “onmouseover” listeners then they will naturally appear in different sections. You can also set your filter preference using the Gear Menu. You can choose to see only the listeners registered on the selected node, or the entire event flow.

Inspect Registered Event Listeners

We are actively looking for UI improvements in this area. So if you have some ideas or feedback please feel free to let us know on this bug report!

Related Bug Reports: Event Listeners.

Syntax Highlighting #

Syntax highlighting enhances readability, makes debugging code easier, and looks really awesome. The Web Inspector now includes syntax highlighting for JSON and CSS.

CSS Syntax Highlighting

CSS Syntax Highlighting even works on the more complex “at-rules” such as @import, @media and @font-face. In addition to supporting the syntax highlighting in the Resources Panel, inline scripts and styles in the Elements Tree Hierarchy are syntax highlighted!

Inline JavaScript and CSS Syntax Highlighting

Related Bug Reports: JSON Highlighting, CSS Highlighting, and Inline Highlighting.

Breakpoints and Watch Expressions #

The Script Debugger continues to become more powerful and more useful.  We already mentioned the keyboard shortcuts above, but there are plenty of other enhancements.

There is a new Breakpoints Sidebar Pane that allows you to easily monitor and work with your breakpoints across all files without the hassle of searching for them.  Each sidebar entry shows the source line and contains a checkbox that allows you to directly enable or disable the breakpoint. Clicking on the entry will jump you directly to the highlighted line in the source file. Finally, deleting a breakpoint has been made easier by clicking the “blue tag” breakpoint indicator.  The tag will cycle through its three states of active, inactive, and removed.

A powerful feature added to the debugger is Conditional Breakpoints. Once you have a breakpoint set, right click on the “blue tag” breakpoint indicator and you will get a popup asking for a conditional statement for that breakpoint. Simply provide an expression and the breakpoint will only pause from then on only if the condition is true.

Breakpoints and other Debugging Improvements

Another new feature in the Debugger is Watch Expressions.  In this new Sidebar Pane you can add any number of expressions that evaluate in the global scope normally but in the local scope when paused in the debugger. Once added you get the full Object Properties tree view of the values of each expression.  These watch expressions automatically refresh when the debugger pauses. They are also persist across page loads.

Watched Expressions in Action

Related Bug Reports: Breakpoints Sidebar PaneWatch Expressions, Evaluate on Breakpoint, Conditional Breakpoints, and Delete Breakpoints.

Debugging AJAX #

An extremely valuable feature for developers working with AJAX is the ability to view the exact parameters and payload sent on XMLHttpRequests.

In the individual resource view there are new sections for viewing submitted Form Data, Query String Parameters, and Request Payloads when appropriate. You can toggle viewing the information in its unencoded (default) and encoded forms with a double-click.

There is also new section named HTTP Information which contains the Request Method (GET, POST, etc.) and the Status Code (200, 404, etc.). Additionally, it adds a colored dot next to the requested URL to show the status (green for success, orange for redirect, and red for error).

View Submitted Form Data and HTTP Information

Related Bug Reports: HTTP Status Code and Data, Parameters, and Payload

Resources and Console Scope Bars #

In order to filter through the Resources or Console messages the Web Inspector now sports some familiar Scope Bars. This has proven to be very useful in the Resources Panel for easily viewing all resources of a particular type.

Quick Filtering Scope Bars for Resources Types and Console Message Types

Related Bug Reports: Resources Scope Bar and Console Scope Bars.

Resources Timeline #

The Web Inspector now specifically shows in the timeline when the DOMContentLoaded and Load events fire. This helps clarify the time it takes for pages to load and helps you improve your websites load times.

DOMContent Ready Event and Page's Load Event Indicators

Related Bug Reports: Show Load Lines

Resources Interactivity #

A couple new features allow you to more directly access individual resources from within the Web Inspector. Instead of copying their URL and opening a new tab manually you can now double-click the Resource in the sidebar to open it directly in a new window. Or, you can drag and drop the resource using HTML5 drag and drop events!

Related Bug Reports: Open Resource Directly and Drag and Drop.

Console Improvements #

Properties in the Web Inspector’s Console are now sorted in a much more natural and useful way. By sorting keys alphanumerically Arrays with greater then 10 elements are much easier to work with.

Alphanumeric Sorting

Another tweak is that collections such as NodeLists and HTMLCollections are now displayed like Arrays.  This meaning that their contents are shown directly in the console, no longer requiring any extra boilerplate.

More Descriptive Nodelists

Related Bug Reports: Sorting and NodeLists.

Firebug Command API Improvements #

More improvements have been made to support more of the Firebug Command Line API. The Web Inspector now supports the inspect() function, which can take an Element, Database, or Storage area and automatically jumps to the appropriate Panel with information. Also, the $0-$4 variables contain the current and previous selected nodes from the Elements Tree Hierarchy.

These command line APIs are usable inside the Web Inspector’s Console. To make working with these APIs even easier, they now show up in the Console’s autocompletion.

Related Bug Reports: $# Variables, inspect() Function, and Autocompletion.

How You Can Contribute #

Many of these new features were added by members of the Open Source Community. We would like to encourage you to contribute as well! Since the Web Inspector itself is mostly HTML, JavaScript, and CSS that means that you already have the skills you need to join in! Interested? Play around right now by inspecting the inspector itself!

Work on the Web Inspector using the Web Inspector!

If you’re interested in contributing and have any questions please stop by the #webkit-inspector IRC channel! As an encouragement to developers, included at the end of each section above are the core bug reports that were involved in bringing each of these features to life.

Finally, if you have ideas for new features, any improvements, or if you’ve stumbled across a bug then please don’t hesitate to create a bug report. This link has pre-populated most of the fields so that you only need to fill out the Summary and Description. As always you should do a quick search through the existing inspector bugs first.

WebGL Now Available in WebKit Nightlies

Posted by Chris Marrin on Monday, October 19th, 2009 at 1:55 pm

Introduction

WebGL is a new standard being worked on in the Khronos consortium. The work done in Khronos is only available to its members, so I can’t show you the spec just yet. But it will become public within the next few months after a review by Khronos members. The good news is that WebGL is now available in WebKit nightlies as of October 4, 2009 (r49073). So if you’re running Leopard or Snow Leopard you can try it out for yourself. WebGL runs in the HTML Canvas element, so it works very similarly to the 2D Canvas capability currently in WebKit.

OpenGL for the Web

OpenGL has been around for ages, so it’s very mature. It can handle all the features of the most advanced graphics cards, but works across a wide variety of hardware. WebGL is based on OpenGL ES 2.0 which is a shader based API.

WebGL is a very low level API, so it’s not for the faint of heart. OpenGL’s shading language, GLSL, is itself an entire programming environment. So doing even simple things in WebGL takes a lot of code. You have to load, compile and link the shaders, setup vertex buffer objects to hold the shapes, and setup the variables to be passed into the shaders. Then you have to do matrix math to animate the shapes. If you want to learn more about all this before continuing, head over to the OpenGL Site for some nice tutorials.

Getting Started

WebGL is really cool! But because it’s new and still under development it isn’t turned on by default. To do that, you need to go into Terminal and type this:

    defaults write com.apple.Safari WebKitWebGLEnabled -bool YES

Once you’ve done that, restart the WebKit nightly build. Then click on the image below. If you see a spinning cube, you have WebGL installed and enabled. If not go back and make sure you have the latest Safari and you typed the above line correctly.


Image of Spinning Box


Click to see Spinning Box (requires WebGL)

If you don’t have a nightly build, you can still see WebGL in action here.

A Simple Example

Let’s see how to create the spinning cube above. For the examples and demos here I’ve created a couple of files of JavaScript utilities to help out: one with some general utilities and another with a set of matrix functions. These will let us focus on the different steps needed to use WebGL without worrying about the details.

Like I said, WebGL is built on top of the Canvas Element. So just like you do for a 2D Canvas you start out by getting a CanvasRenderingContext with a call to the getContext method of the Canvas Element, passing the string “webkit-3d” (this is temporary, and will eventually change to “webgl”). The returned object has a set of functions very similar to OpenGL ES 2.0.

Using Shaders

Nothing happen in WebGL without shaders. They take shape data and turn it into pixels on the screen. When using GLSL you define two separate shaders. The vertex shader runs on each corner of every triangle being rendered. Here you transform the points, pass along the texture coordinates and use the normals to compute a lighting factor based on the normals of each triangle. There is a really nice GLSL Tutorial on lighting. GLSL gives you one special variable to store the transformed corner point, gl_Position. The value stored there for each of the corners of a triangle is used to interpolate all the pixels being output. The texture coordinates and lighting factor are passed in varying variables we created for the purpose.

All these values are passed to the fragment shader, which runs on each pixel of every transformed triangle passed in. This is where you get the appropriate pixel from the texture, adjust its lighting, and output the pixel. GLSL gives you a special variable for this, gl_FragColor. Whatever color your store there will be the color of that pixel.

So let’s define the shaders. I’ll use normal script notation here, even though HTML ignores it. It’s a useful way to include GLSL. The contents of the script will be passed as a string to the shaderSource function:

<script id="vshader"type="x-shader/x-vertex">
        uniform mat4 u_modelViewProjMatrix;
        uniform mat4 u_normalMatrix;
        uniform vec3 lightDir;

        attribute vec3 vNormal;
        attribute vec4 vTexCoord;
        attribute vec4 vPosition;

        varying float v_Dot;
        varying vec2 v_texCoord;

        void main()
        {
            gl_Position = u_modelViewProjMatrix * vPosition;
            v_texCoord = vTexCoord.st;
            vec4 transNormal = u_normalMatrix * vec4(vNormal,1);
            v_Dot = max(dot(transNormal.xyz, lightDir), 0.0);
        }

</script>

<script id="fshader" type="x-shader/x-fragment">

        uniform sampler2D sampler2d;

        varying float v_Dot;
        varying vec2 v_texCoord;

        void main()
        {
            vec2 texCoord = vec2(v_texCoord.s, 1.0 - v_texCoord.t);
            vec4 color = texture2D(sampler2d,texCoord);
            color += vec4(0.1,0.1,0.1,1);
            gl_FragColor = vec4(color.xyz * v_Dot, color.a);
        }

</script>

The vertex shader in this example simply sends along the vertex position, vPosition to the fragment shader after transforming it by a composite model-view/projection matrix. We’ll get to that later. Then it passes along the texture coodinate, vTexCoord, and uses the normal in vNormal to compute a lighting factor, v_Dot for the fragment shader. The fragment shader is even simpler. It just gets a pixel from the texture, (after flipping the texture coordinate so the image is right-side up). Then multiplies that by the lighting factor passed in from the vertex shader. This causes the pixels to be brighter when a side of the cube is facing you and darker when it is at an angle, giving it a realistic lighting effect.

Initializing the Engine

Now we have to get WebGL up and running. The utility library we first loaded will help us here:

    function init()
    {
        // Initialize
        var gl = initWebGL(
                // The id of the Canvas Element
                "example1",
                // The ids of the vertex and fragment shaders
                "vshader", "fshader",
                // The vertex attribute names used by the shaders.
                // The order they appear here corresponds to their index
                // used later.
                [ "vNormal", "vTexCoord", "vPosition"],
                // The clear color and depth values
                [ 0, 0, 0, 1 ], 10000);

        // Set some uniform variables for the shaders
        gl.uniform3f(gl.getUniformLocation(gl.program, "lightDir"), 0, 0, 1);
        gl.uniform1i(gl.getUniformLocation(gl.program, "sampler2d"), 0);

        // Enable texturing
        gl.enable(gl.TEXTURE_2D);

        // Create a box. On return 'gl' contains a 'box' property with the 
        // BufferObjects containing the arrays for vertices, normals, texture 
        // coords, and indices.
        gl.box = makeBox(gl);

        // Load an image to use. Returns a CanvasTexture object
        spiritTexture = loadImageTexture(gl, "spirit.jpg");

        // Create some matrices to use later and save their locations in the shaders
        gl.mvMatrix = new CanvasMatrix4();
        gl.u_normalMatrixLoc = gl.getUniformLocation(gl.program, "u_normalMatrix");
        gl.normalMatrix = new CanvasMatrix4();
        gl.u_modelViewProjMatrixLoc =
                gl.getUniformLocation(gl.program, "u_modelViewProjMatrix");
        gl.mvpMatrix = new CanvasMatrix4();

        // Enable all the vertex arrays
        gl.enableVertexAttribArray(0);
        gl.enableVertexAttribArray(1);
        gl.enableVertexAttribArray(2);

        // Setup all the vertex attributes for vertices, normals and texCoords
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.box.vertexObject);
        gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, gl.box.normalObject);
        gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, gl.box.texCoordObject);
        gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);

        // Bind the index array
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.box.indexObject);

        return gl;
    }

After this initialization we have the shaders loaded and attached to a GLSL program, which is how you define the interface to your shaders. You pass uniforms to a shader for values that don’t change, and vertex attributes for things that do, like vertices. Most of this is taken care of in the library, but you can pass additional values here, like we do with the lightDir and sampler2d uniforms. Here we also tell WebGL that we want to use the arrays the makeBox() function set up containing the vertices, normals and texture coordinates.

Setting Up the Viewport

Before we can render, we have to tell the canvas how to map the objects we are drawing from modeling coodinates, which is the coordinate space we defined the box in, to viewport coordinates. We do that with a transformation matrix. We will use a perspective projection which will make closer objects look larger than further ones, just like in the real world. Here we will use the matrix library we loaded:

    function reshape(gl)
    {
        var canvas = document.getElementById('example1');
        if (canvas.clientWidth == width && canvas.clientHeight == height)
            return;

        width = canvas.clientWidth;
        height = canvas.clientHeight;

        // Set the viewport and projection matrix for the scene
        gl.viewport(0, 0, width, height);
        gl.perspectiveMatrix = new CanvasMatrix4();
        gl.perspectiveMatrix.lookat(0,0,7, 0, 0, 0, 0, 1, 0);
        gl.perspectiveMatrix.perspective(30, width/height, 1, 10000);
    }

We save the perspectiveMatrix for use later. It transforms from world coordinates to viewport coordinates. We will go from modeling coordinate to world coordinates in the next step.

Drawing the Box

Now we’re all set up and we can finally draw our box. Most of the hard work is done but we still have to tell the box we want it to spin, and to do that we define a model-view matrix, which transforms from modeling coordinates to world coordinates. This tells the box where and at what angle we want it to appear. Then we multiply that by the perspective matrix we saved before to complete the transformation all the way from modeling coordinates to viewport coordinates. We also turn the model-view matrix into a normal matrix so it can be used to compute the proper lighting on the box:

    function drawPicture(gl)
    {
        // Make sure the canvas is sized correctly.
        reshape(gl);

        // Clear the canvas
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        // Make a model/view matrix.
        gl.mvMatrix.makeIdentity();
        gl.mvMatrix.rotate(currentAngle, 0,1,0);
        gl.mvMatrix.rotate(20, 1,0,0);

        // Construct the normal matrix from the model-view matrix and pass it in
        gl.normalMatrix.load(gl.mvMatrix);
        gl.normalMatrix.invert();
        gl.normalMatrix.transpose();
        gl.uniformMatrix4fv(gl.u_normalMatrixLoc, false,
                gl.normalMatrix.getAsCanvasFloatArray());

        // Construct the model-view * projection matrix and pass it in
        gl.mvpMatrix.load(gl.mvMatrix);
        gl.mvpMatrix.multRight(gl.perspectiveMatrix);
        gl.uniformMatrix4fv(gl.u_modelViewProjMatrixLoc, false,
                gl.mvpMatrix.getAsCanvasFloatArray());

        // Bind the texture to use
        gl.bindTexture(gl.TEXTURE_2D, spiritTexture);

        // Draw the cube
        gl.drawElements(gl.TRIANGLES, gl.box.numIndices, gl.UNSIGNED_BYTE, 0);

        // Finish up.
        gl.flush();

        // Show the framerate
        framerate.snapshot();

        currentAngle += incAngle;
        if (currentAngle > 360)
            currentAngle -= 360;
    }

Once this is all done you simply add a JavaScript timer to keep changing the angle and rendering the box in its new position and you have a spinning box!

Where to Next?

So, as you can see there’s a lot to learn about 3D rendering. There are some nice tutorials at the OpenGL Site. Most of these are not specific to OpenGL ES 2.0, so you’ll have to figure out what features are and are not available. Unfortunately there aren’t a lot of specific ES 2.0 examples yet. But I think these tutorials will give you a good start. There’s also a great book specifically about OpenGL ES 2.0 called the OpenGL ES 2.0 Programming Guide.

There are also a few WebGL examples in the wild already. Check them out here, here and here. WebKit has a few samples as well:

Spinning Box

Earth

Many Planets

Teapot per-vertex

Teapot per-pixel

WebGL+CSS Animation

Pavel Feldman and Dmitry Titov are now WebKit reviewers.

Posted by David Levin on Tuesday, October 13th, 2009 at 2:51 pm

Pavel has contributed some great new features for the Web Inspector and spent a lot of time on the Web Inspector to help unfork the Chromium port. Dmitry has worked on timers, workers, test improvements, and various other enhancements.

Please join me in congratulating Pavel and Dmitry on their reviewer status!

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…