Asset Preloading and Resource Hints with HTTP/2 and WebLink
Symfony provides native support (via the WebLink component)for managing Link
HTTP headers, which are the key to improve the applicationperformance when using HTTP/2 and preloading capabilities of modern web browsers.
Link
headers are used in HTTP/2 Server Push and W3C's Resource Hintsto push resources (e.g. CSS and JavaScript files) to clients before they evenknow that they need them. WebLink also enables other optimizations that workwith HTTP 1.x:
- Asking the browser to fetch or to render another web page in the background;
- Making early DNS lookups, TCP handshakes or TLS negotiations.Something important to consider is that all these HTTP/2 features require asecure HTTPS connection, even when working on your local machine. The main webservers (Apache, Nginx, Caddy, etc.) support this, but you can also use theDocker installer and runtime for Symfony created by Kévin Dunglas, from theSymfony community.
Preloading Assets
Imagine that your application includes a web page like this:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>My Application</title>
- <link rel="stylesheet" href="/app.css">
- </head>
- <body>
- <main role="main" class="container">
- <!-- ... -->
- </main>
- </body>
- </html>
Following the traditional HTTP workflow, when this page is served browsers willmake one request for the HTML page and another request for the linked CSS file.However, thanks to HTTP/2 your application can start sending the CSS filecontents even before browsers request them.
To do that, first install the WebLink component:
- $ composer require symfony/web-link
Now, update the template to use the preload()
Twig function provided byWebLink. The "as" attribute is mandatory because browsers need it to applycorrect prioritization and the content security policy:
- <head>
- <!-- ... -->
- <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style' }) }}">
- </head>
If you reload the page, the perceived performance will improve because theserver responded with both the HTML page and the CSS file when the browser onlyrequested the HTML page.
Note
You can preload an asset by wrapping it with the preload()
function:
- <head>
- <!-- ... -->
- <link rel="stylesheet" href="{{ preload(asset('build/app.css')) }}">
- </head>
Additionally, according to the Priority Hints specification, you can signalthe priority of the resource to download using the importance
attribute:
- <head>
- <!-- ... -->
- <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', importance: 'low' }) }}">
- </head>
How does it work?
The WebLink component manages the Link
HTTP headers added to the response.When using the preload()
function in the previous example, the followingheader was added to the response: Link </app.css>; rel="preload"; as="style"
According to the Preload specification), when an HTTP/2 server detects thatthe original (HTTP 1.x) response contains this HTTP header, it willautomatically trigger a push for the related file in the same HTTP/2 connection.
Popular proxy services and CDNs including Cloudflare, Fastly and Akamaialso leverage this feature. It means that you can push resources to clients andimprove performance of your applications in production right now.
If you want to prevent the push but let the browser preload the resource byissuing an early separate HTTP request, use the nopush
option:
- <head>
- <!-- ... -->
- <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}">
- </head>
Resource Hints
Resource Hints are used by applications to help browsers when deciding whichresources should be downloaded, preprocessed or connected to first.
The WebLink component provides the following Twig functions to send those hints:
dns_prefetch()
: "indicates an origin (e.g.https://foo.cloudfront.net
)that will be used to fetch required resources, and that the user agent shouldresolve as early as possible".preconnect()
: "indicates an origin (e.g.https://www.google-analytics.com
)that will be used to fetch required resources. Initiating an early connection,which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allowsthe user agent to mask the high latency costs of establishing a connection".prefetch()
: "identifies a resource that might be required by the nextnavigation, and that the user agent should fetch, such that the user agentcan deliver a faster response once the resource is requested in the future".prerender()
: "identifies a resource that might be required by the nextnavigation, and that the user agent should fetch and execute, such that theuser agent can deliver a faster response once the resource is requested later".The component also supports sending HTTP links not related to performance andany link implementing the PSR-13 standard. For instance, anylink defined in the HTML specification:
- <head>
- <!-- ... -->
- <link rel="alternate" href="{{ link('/index.jsonld', 'alternate') }}">
- <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}">
- </head>
The previous snippet will result in this HTTP header being sent to the client:Link: </index.jsonld>; rel="alternate",</app.css>; rel="preload"; nopush
You can also add links to the HTTP response directly from controllers and services:
- // src/Controller/BlogController.php
- namespace App\Controller;
- use Fig\Link\GenericLinkProvider;
- use Fig\Link\Link;
- use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- use Symfony\Component\HttpFoundation\Request;
- class BlogController extends AbstractController
- {
- public function index(Request $request)
- {
- // using the addLink() shortcut provided by AbstractController
- $this->addLink($request, new Link('preload', '/app.css'));
- // alternative if you don't want to use the addLink() shortcut
- $linkProvider = $request->attributes->get('_links', new GenericLinkProvider());
- $request->attributes->set('_links', $linkProvider->withLink(new Link('preload', '/app.css', ['as' : 'style'])));
- return $this->render('...');
- }
- }
WebLink can be used as a standalone PHP librarywithout requiring the entire Symfony framework.