Using HTTP/2 Server Push with PHP

by John Graham-Cumming.

Two weeks ago CloudFlare announced that it was supporting HTTP/2 Server Push for all our customers. By simply adding a Link header to an HTTP response specifying preload CloudFlare would automatically push items to web browsers that support Server Push.

To illustrate how easy this is I create a small PHP page that uses the PHP header function to insert appropriate Link headers to push images to the web browser via CloudFlare. The web page looks like this when loaded:

There are two images loaded from the same server both of which are pushed if the web browser supports Server Push. This is achieved by inserting two Link headers in the HTTP response. The response looks like:

HTTP/1.1 200 OK
Server: nginx/1.9.15
Date: Fri, 13 May 2016 10:52:13 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Link: </images/drucken.jpg>; rel=preload; as=image
Link: </images/empire.jpg>; rel=preload; as=image

At the bottom are the two Link headers corresponding to the two images on the page with the rel=preload directive as specified in W3C preload draft.

The complete code can be found in this gist but the core of the code looks like this:

    <?php
    function pushImage($uri) {
        header("Link: <{$uri}>; rel=preload; as=image", false);
        return <<<HTML
    <img src="{$uri}">
    HTML;
    }

    $image1 = pushImage("/images/drucken.jpg");
    $image2 = pushImage("/images/empire.jpg");
    ?>

    <html>

    <head><title>PHP Server Push</title></head>
    <body>

    <h1>PHP Server Push</h1>

    <?php
    echo ccbysa($image1, "https://bit.ly/1Wu5bYx",
       "https://www.flickr.com/photos/hiperactivo/", "Javier Candeira");

    echo ccbynd($image2, "https://bit.ly/24PHue3",
        "https://www.flickr.com/photos/bobsfever/", "Robert McGoldrick");
    ?>

    </body>
    </html>

Since you have to call the PHP header function before any output (such as HTML) has occurred the code makes two calls to a helper function called pushImage first. pushImage adds the appropriate Link header and returns the HTML needed to insert the actual image in the page.

Later the variables $image1 and $image2 (which contain the HTML needed to display the images) is inserted use two other helper functions (ccbysa and ccbynd) that add captions. Those two helper functions don't play any part in the Server Push, they just ensure that the HTML is placed correctly on the page with a caption.

Notice that the Link header is added as follows:

header("Link: <{$uri}>; rel=preload; as=image", false);

The false second parameter tells header to not override an existing Link header. With that option specified multiple Link headers can be added. Without it the last call to pushImage would win.

Effect of Server Push

To understand the effect of Server Push on the example I used Google Chrome Canary and loaded the page twice (once without the Link headers so that no push would occur and once with).

Here's the simple waterfall over HTTP/2 with no Server Push:

The page load time was 651ms. You can see the initial page HTML load in 175ms followed by the two images.

How here's the waterfall with HTTP/2 Server Push:

The page load time was 251ms (the HTML loaded in 168ms) and Chrome is showing that the two images were pushed (see the Initiator column).

It's not 100% obvious what happened there but digging into chrome://net-internals/ it's possible to see a detailed HTTP/2 timeline. I've edited out a few details of the protocol (such as the window size changes) to focus in on the requests and responses.

The t value gives the tick time (1ms per tick).

t=910212 [st=   1]    HTTP2_SESSION_SEND_HEADERS
                      --> exclusive = true
                      --> fin = true
                      --> has_priority = true
                      --> :method: GET
                          :scheme: https
                          :path: /index.php
                          pragma: no-cache
                          cache-control: no-cache
                          upgrade-insecure-requests: 1
                          accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
                          accept-encoding: gzip, deflate, sdch, br
                          accept-language: en-US,en;q=0.8
                          cookie: [52 bytes were stripped]
                      --> parent_stream_id = 0
                      --> priority = 0
                      --> stream_id = 1

[...]

t=910404 [st= 193]    HTTP2_SESSION_RECV_HEADERS
                      --> fin = false
                      --> :status: 200
                          date: Fri, 13 May 2016 11:28:16 GMT
                          content-type: text/html
                          content-encoding: gzip
                      --> stream_id = 1
t=910405 [st= 194]    HTTP2_SESSION_RECV_PUSH_PROMISE
                      --> :method: GET
                          :path: /images/drucken.jpg
                          :scheme: https
                          accept-encoding: gzip, deflate, sdch, br
                      --> id = 1
                      --> promised_stream_id = 2
t=910405 [st= 194]    HTTP2_SESSION_RECV_PUSH_PROMISE
                      --> :method: GET
                          :path: /images/empire.jpg
                          :scheme: https
                          accept-encoding: gzip, deflate, sdch, br
                      --> id = 1
                      --> promised_stream_id = 4
t=910405 [st= 194]    HTTP2_SESSION_RECV_DATA
                      --> fin = false
                      --> size = 298
                      --> stream_id = 1
t=910405 [st= 194]    HTTP2_SESSION_RECV_DATA
                      --> fin = true
                      --> size = 0
                      --> stream_id = 1
t=910409 [st= 198]    HTTP2_SESSION_RECV_HEADERS
                      --> fin = false
                      --> :status: 200
                          date: Fri, 13 May 2016 11:28:16 GMT
                          content-type: image/jpeg
                          content-length: 49852
                          set-cookie: [124 bytes were stripped]
                          etag: "5735aac0-12f99"
                          last-modified: Fri, 13 May 2016 10:21:52 GMT
                          cf-cache-status: HIT
                          vary: Accept-Encoding
                          expires: Fri, 13 May 2016 15:28:16 GMT
                          cache-control: public, max-age=14400
                          accept-ranges: bytes
                          server: cloudflare-nginx
                      --> stream_id = 2
t=910409 [st= 198]    HTTP2_SESSION_RECV_DATA
                      --> fin = false
                      --> size = 987
                      --> stream_id = 2
t=910409 [st= 198]    HTTP2_SESSION_RECV_DATA
                      --> fin = false
                      --> size = 1369
                      --> stream_id = 2
t=910410 [st= 199]    HTTP2_SESSION_RECV_DATA
                      --> fin = false
                      --> size = 1369
                      --> stream_id = 2

The web browser sends an HTTP2_SESSION_SEND_HEADERS request asking for the web page (that's the first item above) and subsequently receives the response headers and page followed immediately by two push promises for the two images. It then immediately starts receiving image data (see the stream_id 2).

CloudFlare's web server is pushing the image contents before the browser asked for them. When CloudFlare pushes an item specified in a Link header the header itself is stripped (to prevent the browser from re-requesting the resource).

The Future Starts Here

We released HTTP/2 Server Push support to help kick start innovative use of this critical feature of HTTP/2. We would love people to start experimenting with it to see how much of a speed improvement is possible with their specific websites.

As can be seen from this blog post making use of Server Push in a web application is easy: just insert Link headers with the appropriate format. Server Push will be particularly fast if the items being pushed are also stored in CloudFlare's cache (as was the case with the examples above).

Please let us know how you use Server Push. We're particularly interested in experiences with pushing different types of resources (images vs. styles vs. scripts) and working out the optimal number of items to push (we currently allow up to 50 resources per page).

comments powered by Disqus