A few months ago, I’ve contributed a new component to Symfony: WebLink. By implementing cutting edge web standards, namely HTTP/2 Server Push and W3C’s Resource Hints, it brings great opportunities to boost webapp’s performance.
Thanks to Symfony WebLink, HTTP/2 (h2) servers are able to push resources to clients before they even know that they need them (think to CSS or JavaScript files, or relations of an API resource). WebLink also enables other very efficient optimisations that work with HTTP 1:
- telling the browser to fetch or to render another webpage in the background ;
- init early DNS lookups, TCP handshakes or TLS negotiations
Let’s discover how easy it is to use it and the real life benefits you can expect. The WebLink components is available since Symfony 3.3 (it means you can start to use it today), but in this article I’ll use Symfony 4 to celebrate the release of its first beta yesterday!
To get started, download my Docker installer and runtime for Symfony 4/Flex. It includes everything you need to run Symfony (PHP 7.1 configured properly for Symfony, and Composer) and a development reverse proxy (Apache) supporting HTTP/2 Server Push and HTTPS (most clients only support HTTP/2 over TLS).
Unzip the downloaded archive, open a shell in the resulting directory and run the following commands:
# Optional: use Symfony 4 beta (will default to Symfony 3.3 if you omit this line) $ docker-compose build --build-arg STABILITY=beta app # Install Flex and start the project $ docker-compose up
Open https://localhost
, if this nice page appears, you successfully created your first Symfony 4 project and are browsing it in HTTP/2!Let’s create a very simple homepage using the Twig templating engine. Because Symfony Flex starts as a micro framework, the first step is to install the library itself: docker-compose exec app composer req twig
(run this in a new shell)
Flex is smart enough to download Twig, automatically register it into Symfony and enable Symfony features requiring the library. It also generates a base HTML5 layout in the templates/
directory (we will not use it but you should).
Now, download Bootstrap 4, extract the archive and copy the file dist/css/bootstrap.min.css
in the public/
directory of our project. As you may know Symfony already has a nice integration with the upcoming version 4 of the most popular CSS framework.
Note: in a real project, you should use Yarn or NPM with Symfony Encore to install Bootstrap.
Now, it’s time to create the template of our homepage:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Welcome!</title> <link rel="stylesheet" href="/bootstrap.min.css"> </head> <body> <main role="main" class="container"> <h1>Hello World</h1> <p class="lead">That's a lot of highly dynamic content, right?</p> </main> </body> </html>
And finally, register our new template as the homepage using the builtin TemplateController:
# config/routes.yaml index: path: / defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction' template: 'homepage.html.twig'
Clear the cache: docker-compose exec app bin/console c:c
(this step will become optional when #24642 will be merged)
Refresh your browser, this nice HP should appear:
2 HTTP requests are issued by the browser, one for the homepage, and another one for Bootstrap.
But we know from the very beginning that the browser will need Bootstrap. Instead of waiting that the browser downloads the homepage, parses the HTML (notice “Initiator: Parser” in Chrome DevTools), encounters the reference to bootstrap.min.css
and finally sends a new HTTP request, we could take benefit of the HTTP/2 Push feature to directly send both resources to the browser. Let’s do it!
Install the WebLink component: docker-compose exec app composer req weblink
As for Twig, Flex will automatically download and register this component into our app.
Now, update the template to use the preload
Twig helper that leverages the WebLink component:
<!-- ... --> <link rel="stylesheet" href="{{ preload('/bootstrap.min.css') }}"> <!-- ... -->
Reload the page:As you can see (Initiator: Push), both responses have been sent directly by the server. bootstrap.min.css
has started to be received before the browser even requested it!
How does it works?
The WebLink component tracks Link HTTP headers to add to the response. When using the preload()
helper, a Link
header with a preload rel
attribute is added to the response:
According to the Preload specification, when a HTTP/2 server detects that the original (HTTP 1) response contains this HTTP header, it will automatically trigger a push for the related file in the same HTTP/2 connection.
The Apache server provided by my Docker setup supports this feature. It’s why Bootstrap is pushed to the client! Popular proxy services and CDN including Cloudflare, Fastly and Akamai also leverage this feature. It means that you can push resources to clients and improve performance of your apps in production right now! All you need is Symfony 3.3+ and a compatible web server (the free version of Nginx doesn’t support Server Push) or CDN service.
If you want to prevent the push but let the browser preload the resource by issuing an early separate HTTP request, use the nopush
attribute:
<!-- ... --> <link rel="stylesheet" href="{{ preload('/bootstrap.min.css', {nopush: true}) }}"> <!-- ... -->
Before using HTTP/2 Push, be sure to read this great article about known issues, cache implications and the state of the support in popular browsers.
In addition to HTTP/2 Push and preloading, the WebLink component also provide some helpers to send Resource Hints to clients, the following helpers are available:
dns_prefetch
: “indicate an origin that will be used to fetch required resources, and that the user agent should resolve as early as possible”preconnect
: “indicate an origin that will be used to fetch required resources. Initiating an early connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user agent to mask the high latency costs of establishing a connection”prefetch
: “identify a resource that might be required by the next navigation, and that the user agent should fetch, such that the user agent can deliver a faster response once the resource is requested in the future”prerender
: “identify a resource that might be required by the next navigation, and that the user agent should fetch and execute, such that the user agent can deliver a faster response once the resource is requested in the future”
The component can also be used to send HTTP link not related to performance. For instance, any link defined in the HTML specification:
<!-- ... --> <link rel="alternate" href="{{ link('/index.jsonld', 'alternate') }}"> <link rel="stylesheet" href="{{ preload('/bootstrap.min.css', {nopush: true}) }}"> <!-- ... -->
The previous snippet will result in this HTTP header being sent to the client:
Link: </index.jsonld>; rel="alternate",</bootstrap.min.css>; rel="preload"; nopush
You can also add links to the HTTP response directly from a controller or any service:
<?php // src/Controller/BlogPostAction.php namespace App\Controller; use Fig\Link\GenericLinkProvider; use Fig\Link\Link; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; final class BlogPostAction { public function __invoke(Request $request): Response { $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); $request->attributes->set('_links', $linkProvider->withLink(new Link('preload', '/bootstrap.min.css'))); return new Response('Hello'); } } # app/config/routes.yaml blog_post: path: /post defaults: _controller: 'App\Controller\BlogPostAction'
Last but not least, as all Symfony components, WebLink can be used as a standalone PHP library:
<?php require __DIR__.'/../vendor/autoload.php'; use Fig\Link\GenericLinkProvider; use Fig\Link\Link; use Symfony\Component\WebLink\HttpHeaderSerializer; $linkProvider = (new GenericLinkProvider()) ->withLink(new Link('preload', '/bootstrap.min.css')); header('Link: '.(new HttpHeaderSerializer())->serialize($linkProvider->getLinks())); echo 'Hello';
It is already as a standalone library by the Bolt CMS. While we’re speaking about interoperability, WebLink can deal with any link implementing PSR-13.
Thanks to Symfony WebLink, there is no excuses to not to switch to HTTP/2!
Learn more about these new features in the relevant Pull Requets: #22273 and #21478.
Your Symfony app is slow? Contact Les-Tilleuls.coop, we can help you make it fast!
Can I get the contact person to join and get more information?
In the BlogPostAction.php example, you are adding the attributes to the request, not the response – I suspect this is an error?
And if you add the attribute _links to the response, does Symfony automatically convert that into Link HTTP headers?
I want to use this Twig Extension. But I’am running in an error with symfony referring Fig\Link\Link; Output: ClassNotFoundException
Attempted to load class “Link” from namespace “Fig\Link”.
Did you forget a “use” statement for e.g. “phpDocumentor\Reflection\DocBlock\Tags\Link” or “Symfony\Component\DomCrawler\Link”?
twig-bridge is already installed via composer.
Do you know how this issue can be resolved?