V1.3: HTTP/2 Early Hints


A web page may link to external resources such as stylesheets, or images (assets). With HTTP/1.1 the browser parses the HTML and for each link, it downloads the asset and eventually take an action on it: renders an image or evaluate JavaScript code. With HTTP/2 introduced an enhancement: the server can proactively “push” in parallel both the HTML payload and some assets to the browser. This workflow is allowed due to the HTTP/2 TCP connections are multiplexed. That means many communications can happen at the same time.

Unfortunately HTTP/2 adoption is still slow, so the IETF “backported” this workflow to HTTP/1.1 as well, by introducing the HTTP status 103 Early Hints. In this case the server sends one or more HTTP responses for a single request. The last one must be the traditional 200 OK that returns the HTML of the page, whereas the first n can include a special header Link to tell the browser to fetch the asset ahead of time.

Setup

As first thing, you need Puma 3.11.0+ with Early-Hints enabled:

# Gemfile
gem "puma"
# config/puma.rb
early_hints true

Then from the project configuration, you can simply enable the feature:

# config/environment.rb
Hanami.configure do
  # ...
  early_hints true
end

As last step, you need a web server that supports HTTP/2 and Early Hints like h2o. When you’ll start the server and visit the page, javascripts and stylesheets will be pushed (see Assets helpers section).

Other web servers

As of today, only Puma supports Early Hints.

Assets helpers

In order to automatically push your assets, you have to use our assets helpers. But given to browser limitations (only up to 100 assets can be pushed), Hanami by default sends stylesheets and javascripts only.

Helper Early Hints asset type Pushed by default
javascript :script yes
stylesheet :style yes
favicon :image no
image :image no
video :video no
audio :audio no
asset_path N/A no
asset_url N/A no

You can opt-in/out the following types:

Javascripts

Pushed by default:

<%= javascript "application" %>
<%= javascript "https://somecdn.test/framework.js", "dashboard" %>

Opt-out:

<%= javascript "application", push: false %>
<%= javascript "https://somecdn.test/framework.css", "dashboard", push: false %>

Stylesheets

Pushed by default:

<%= stylesheet "application" %>
<%= stylesheet "https://somecdn.test/framework.css", "dashboard" %>

Opt-out:

<%= stylesheet "application", push: false %>
<%= stylesheet "https://somecdn.test/framework.css", "dashboard", push: false %>

Favicon

Opt-in:

<%= favicon "favicon.ico", push: :image %>

Image

Opt-in:

<%= image "avatar.png", push: :image %>

Audio

Opt-in:

<%= audio "song.ogg", push: true %>

Block syntax (pushes only song.ogg):

<%=
  audio do
    text "Your browser does not support the audio tag"
    source src: asset_path("song.ogg", push: :audio), type: "audio/ogg"
    source src: asset_path("song.wav"), type: "audio/wav"
  end
%>

Video

Opt-in:

<%= video "movie.mp4", push: true %>

Block syntax (pushes only movie.mp4):

<%=
  video do
    text "Your browser does not support the video tag"
    source src: asset_path("movie.mp4", push: :video), type: "video/mp4"
    source src: asset_path("movie.ogg"), type: "video/ogg"
  end
%>

Asset path

<%= asset_path "application.js", push: :script %>

Asset URL

<%= asset_url "https://somecdn.test/framework.js", push: :script %>

Demo project

If you’re looking for a full working example, please check this demo project.