|
| 1 | +.. index:: |
| 2 | + single: WebLink |
| 3 | + single: Components; WebLink |
| 4 | + |
| 5 | +The WebLink Component |
| 6 | +====================== |
| 7 | + |
| 8 | + The WebLink component provides tools to create `Web Links`_. |
| 9 | + It allows to easily leverage `HTTP/2 Server Push`_ as well as `Resource Hints`_. |
| 10 | + |
| 11 | +.. versionadded:: 3.3 |
| 12 | + The WebLink component was introduced in Symfony 3.3. |
| 13 | + |
| 14 | +By implementing cutting edge web standards, namely `HTTP/2 Server Push`_ and |
| 15 | +W3C's `Resource Hints`_, the WebLink component |
| 16 | +brings great opportunities to boost webapp's performance. |
| 17 | + |
| 18 | +Thanks to WebLink, HTTP/2 (**h2**) servers are able to push resources to clients |
| 19 | +before they even know that they need them (think to CSS or JavaScript |
| 20 | +files, or relations of an API resource). WebLink also enables other very |
| 21 | +efficient optimisations that work with HTTP 1: |
| 22 | + |
| 23 | +- telling the browser to fetch or to render another webpage in the |
| 24 | + background ; |
| 25 | +- init early DNS lookups, TCP handshakes or TLS negotiations |
| 26 | + |
| 27 | +Let's discover how easy it is to use it and the real life benefits you |
| 28 | +can expect. |
| 29 | + |
| 30 | +To benefit from HTTP/2 Server Pushes, a HTTP/2 server and a HTTPS connection |
| 31 | +are mandatory (even in local). |
| 32 | +Both Apache, Nginx and Caddy support these protocols. |
| 33 | +Be sure they are properly configured before reading. |
| 34 | + |
| 35 | +Alternatively, you can use the `Docker installer and runtime for |
| 36 | +Symfony`_ provided by Kévin Dunglas (community supported). |
| 37 | + |
| 38 | +It includes everything you need to run Symfony |
| 39 | +(PHP :doc:`configured properly for Symfony </performance>`, and Composer) |
| 40 | +as well as a development reverse proxy (Apache) supporting HTTP/2 Server Push |
| 41 | +and HTTPS (most clients only support HTTP/2 over TLS). |
| 42 | + |
| 43 | +Unzip the downloaded archive, open a shell in the resulting directory and run |
| 44 | +the following command: |
| 45 | + |
| 46 | +.. code-block:: terminal |
| 47 | +
|
| 48 | + # Install Symfony and start the project |
| 49 | + $ docker-compose up |
| 50 | +
|
| 51 | +Open ``https://localhost``, if this nice page appears, you |
| 52 | +successfully created your first Symfony 4 project and are browsing it in |
| 53 | +HTTP/2! |
| 54 | + |
| 55 | +.. image:: /_images/components/weblink/symfony4-http2.png |
| 56 | + |
| 57 | +Let's create a very simple homepage using |
| 58 | +the `Twig`_ templating engine. |
| 59 | + |
| 60 | +The first step is to install the library itself: |
| 61 | + |
| 62 | +.. code-block:: terminal |
| 63 | +
|
| 64 | + composer req twig |
| 65 | +
|
| 66 | +Symfony is smart enough to download Twig, to automatically register it, |
| 67 | +and to enable Symfony features requiring the library. |
| 68 | +It also generates a base HTML5 layout in the ``templates/`` directory. |
| 69 | + |
| 70 | +Now, download `Bootstrap`_, extract the archive and copy the file |
| 71 | +``dist/css/bootstrap.min.css`` in the ``public/`` directory of our |
| 72 | +project. |
| 73 | +
|
| 74 | +Symfony comes with a `nice integration with of the most popular CSS framework`_. |
| 75 | +
|
| 76 | +.. note:: |
| 77 | +
|
| 78 | + In a real project, you should use Yarn or NPM with |
| 79 | + :doc:`Symfony Encore </frontend/encore/bootstrap>`_ |
| 80 | + to install Bootstrap. |
| 81 | +
|
| 82 | +Now, it's time to create the template of our homepage: |
| 83 | +
|
| 84 | +.. code-block:: html |
| 85 | +
|
| 86 | + <!DOCTYPE html> |
| 87 | + <html> |
| 88 | + <head> |
| 89 | + <meta charset="UTF-8"> |
| 90 | + <title>Welcome!</title> |
| 91 | + <link rel="stylesheet" href="/bootstrap.min.css"> |
| 92 | + </head> |
| 93 | + <body> |
| 94 | + <main role="main" class="container"> |
| 95 | + <h1>Hello World</h1> |
| 96 | + <p class="lead">That's a lot of highly dynamic content, right?</p> |
| 97 | + </main> |
| 98 | + </body> |
| 99 | + </html> |
| 100 | +
|
| 101 | +And finally, register our new template as the homepage using the builtin |
| 102 | +:doc:`TemplateController </templating/render_without_controller>`_: |
| 103 | +
|
| 104 | +.. code-block:: yaml |
| 105 | +
|
| 106 | + # config/routes.yaml |
| 107 | + index: |
| 108 | + path: / |
| 109 | + defaults: |
| 110 | + _controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction' |
| 111 | + template: 'homepage.html.twig' |
| 112 | +
|
| 113 | +Refresh your browser, this nice homepage should appear: |
| 114 | +
|
| 115 | +.. image:: /_images/components/weblink/homepage-requests.png |
| 116 | +
|
| 117 | +HTTP requests are issued by the browser, one for the homepage, and |
| 118 | +another one for Bootstrap. But we know from the very beginning that the |
| 119 | +browser **will** need Bootstrap. Instead of waiting that the browser |
| 120 | +downloads the homepage, parses the HTML (notice "Initiator: Parser" in |
| 121 | +Chrome DevTools), encounters the reference to ``bootstrap.min.css`` and |
| 122 | +finally sends a new HTTP request, we could take benefit of the HTTP/2 |
| 123 | +Push feature to directly send both resources to the browser. |
| 124 | + |
| 125 | +Let's do it! Install the WebLink component: |
| 126 | + |
| 127 | +.. code-block:: terminal |
| 128 | +
|
| 129 | + composer req weblink |
| 130 | +
|
| 131 | +As for Twig, Symfony will automatically download and register this component into our app. |
| 132 | +Now, update the template to use the ``preload`` Twig helper that |
| 133 | +leverages the WebLink component: |
| 134 | +
|
| 135 | +.. code:: html+twig |
| 136 | +
|
| 137 | + {# ... #} |
| 138 | + <link rel="stylesheet" href="{{ preload('/bootstrap.min.css') }}"> |
| 139 | + {# ... #} |
| 140 | +
|
| 141 | +Reload the page: |
| 142 | +
|
| 143 | +.. image:: /_images/components/weblink/http2-server-push.png |
| 144 | +
|
| 145 | +As you can see (Initiator: Push), both |
| 146 | +responses have been sent directly by the server. |
| 147 | +``bootstrap.min.css`` has started to be received before the browser even requested it! |
| 148 | + |
| 149 | +.. note:: |
| 150 | + |
| 151 | + Google Chrome provides a nice interface to debug HTTP/2 connections. |
| 152 | + Open ``chrome://net-internals/#http2`` to start the tool. |
| 153 | + |
| 154 | +How does it works? |
| 155 | +~~~~~~~~~~~~~~~~~~ |
| 156 | + |
| 157 | +The WebLink component tracks Link HTTP headers to add to the response. |
| 158 | +When using the ``preload()`` helper, a ``Link`` header |
| 159 | +with a `preload`_ |
| 160 | +``rel`` attribute is added to the response: |
| 161 | +
|
| 162 | +.. image:: /_images/components/weblink/response-headers.png |
| 163 | +
|
| 164 | +According to `the Preload specification`_, |
| 165 | +when a HTTP/2 server detects that the original (HTTP 1) response |
| 166 | +contains this HTTP header, it will automatically trigger a push for the |
| 167 | +related file in the same HTTP/2 connection. |
| 168 | +The Apache server provided in the Docker setup supports this feature. |
| 169 | +It's why Bootstrap is pushed |
| 170 | +to the client! |
| 171 | +
|
| 172 | +Popular proxy services and CDN including |
| 173 | +`Cloudflare`_, `Fastly`_ and `Akamai`_ also leverage this feature. |
| 174 | +It means that you can push resources to |
| 175 | +clients and improve performance of your apps in production right now! |
| 176 | +All you need is Symfony 3.3+ and a compatible web server or CDN service. |
| 177 | +
|
| 178 | +If you want to prevent the push but let the browser preload the resource by |
| 179 | +issuing an early separate HTTP request, use the ``nopush`` attribute: |
| 180 | + |
| 181 | +.. code-block:: html+twig |
| 182 | + |
| 183 | + {# ... #} |
| 184 | + <link rel="stylesheet" href="{{ preload('/bootstrap.min.css', {nopush: true}) }}"> |
| 185 | + {# ... #} |
| 186 | + |
| 187 | +Before using HTTP/2 Push, be sure to read `this great article`_ about |
| 188 | +known issues, cache implications and the state of the support in popular |
| 189 | +browsers. |
| 190 | + |
| 191 | +In addition to HTTP/2 Push and preloading, the WebLink component also |
| 192 | +provide some helpers to send `Resource |
| 193 | +Hints <https://www.w3.org/TR/resource-hints/#resource-hints>`__ to |
| 194 | +clients, the following helpers are available: |
| 195 | + |
| 196 | +- ``dns_prefetch``: "indicate an origin that will be used to fetch |
| 197 | + required resources, and that the user agent should resolve as early |
| 198 | + as possible" |
| 199 | +- ``preconnect``: "indicate an origin that will be used to fetch |
| 200 | + required resources. Initiating an early connection, which includes |
| 201 | + the DNS lookup, TCP handshake, and optional TLS negotiation, allows |
| 202 | + the user agent to mask the high latency costs of establishing a |
| 203 | + connection" |
| 204 | +- ``prefetch``: "identify a resource that might be required by the next |
| 205 | + navigation, and that the user agent *should* fetch, such that the |
| 206 | + user agent can deliver a faster response once the resource is |
| 207 | + requested in the future" |
| 208 | +- ``prerender``: "identify a resource that might be required by the |
| 209 | + next navigation, and that the user agent *should* fetch and |
| 210 | + execute, such that the user agent can deliver a faster response once |
| 211 | + the resource is requested in the future" |
| 212 | + |
| 213 | +The component can also be used to send HTTP link not related to |
| 214 | +performance. For instance, any `link defined in the HTML specification`_: |
| 215 | + |
| 216 | +.. code:: html+twig |
| 217 | + |
| 218 | + {# ... #} |
| 219 | + <link rel="alternate" href="{{ link('/index.jsonld', 'alternate') }}"> |
| 220 | + <link rel="stylesheet" href="{{ preload('/bootstrap.min.css', {nopush: true}) }}"> |
| 221 | + {# ... #} |
| 222 | + |
| 223 | +The previous snippet will result in this HTTP header being sent to the |
| 224 | +client: |
| 225 | +``Link: </index.jsonld>; rel="alternate",</bootstrap.min.css>; rel="preload"; nopush`` |
| 226 | + |
| 227 | +You can also add links to the HTTP response directly from a controller |
| 228 | +or any service: |
| 229 | + |
| 230 | +.. code:: php |
| 231 | +
|
| 232 | + // src/Controller/BlogPostAction.php |
| 233 | + namespace App\Controller; |
| 234 | +
|
| 235 | + use Fig\Link\GenericLinkProvider; |
| 236 | + use Fig\Link\Link; |
| 237 | + use Symfony\Component\HttpFoundation\Request; |
| 238 | + use Symfony\Component\HttpFoundation\Response; |
| 239 | +
|
| 240 | + final class BlogPostAction |
| 241 | + { |
| 242 | + public function __invoke(Request $request): Response |
| 243 | + { |
| 244 | + $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); |
| 245 | + $request->attributes->set('_links', $linkProvider->withLink(new Link('preload', '/bootstrap.min.css'))); |
| 246 | +
|
| 247 | + return new Response('Hello'); |
| 248 | + } |
| 249 | + } |
| 250 | +
|
| 251 | +.. code-block:: yaml |
| 252 | +
|
| 253 | + # app/config/routes.yaml |
| 254 | + blog_post: |
| 255 | + path: /post |
| 256 | + defaults: |
| 257 | + _controller: 'App\Controller\BlogPostAction' |
| 258 | +
|
| 259 | +Last but not least, as all Symfony components, WebLink can be used as a |
| 260 | +standalone PHP library: |
| 261 | + |
| 262 | +.. code-block:: php |
| 263 | +
|
| 264 | + <?php |
| 265 | +
|
| 266 | + require __DIR__.'/../vendor/autoload.php'; |
| 267 | +
|
| 268 | + use Fig\Link\GenericLinkProvider; |
| 269 | + use Fig\Link\Link; |
| 270 | + use Symfony\Component\WebLink\HttpHeaderSerializer; |
| 271 | +
|
| 272 | + $linkProvider = (new GenericLinkProvider()) |
| 273 | + ->withLink(new Link('preload', '/bootstrap.min.css')); |
| 274 | +
|
| 275 | + header('Link: '.(new HttpHeaderSerializer())->serialize($linkProvider->getLinks())); |
| 276 | +
|
| 277 | + echo 'Hello'; |
| 278 | +
|
| 279 | +To see how WebLink is used in the wild, take a look to the `Bolt`_ |
| 280 | +and `Sulu`_ CMS, they both use WebLink to trigger HTTP/2 pushes. |
| 281 | + |
| 282 | +While we're speaking about interoperability, WebLink can deal with any link implementing |
| 283 | +`PSR-13`_. |
| 284 | + |
| 285 | +Thanks to Symfony WebLink, there is no excuses to not to switch to HTTP/2! |
| 286 | + |
| 287 | +.. _`Web Links`: https://tools.ietf.org/html/rfc5988 |
| 288 | +.. _`HTTP/2 Server Push`: https://tools.ietf.org/html/rfc7540#section-8.2 |
| 289 | +.. _`Resource Hints`: https://www.w3.org/TR/resource-hints/ |
| 290 | +.. _`Twig`: https://twig.symfony.com/ |
| 291 | +.. _`Docker installer and runtime for Symfony`: https://github.com/dunglas/symfony-docker |
| 292 | +.. _`Bootstrap`: https://getbootstrap.com/ |
| 293 | +.. _`nice integration with of the most popular CSS framework`: https://symfony.com/blog/new-in-symfony-3-4-bootstrap-4-form-theme |
| 294 | +.. _`preload`: https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content |
| 295 | +.. _`the Preload specification`: https://www.w3.org/TR/preload/#server-push-(http/2) |
| 296 | +.. _`Cloudflare`: https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/ |
| 297 | +.. _`Fastly`: https://docs.fastly.com/guides/performance-tuning/http2-server-push |
| 298 | +.. _`Akamai`: https://blogs.akamai.com/2017/03/http2-server-push-the-what-how-and-why.html |
| 299 | +.. _`this great article`: https://www.shimmercat.com/en/blog/articles/whats-push/ |
| 300 | +.. _`link defined in the HTML specification`: https://html.spec.whatwg.org/dev/links.html#linkTypes |
| 301 | +.. _`Bolt`: https://bolt.cm/ |
| 302 | +.. _`Sulu`: https://sulu.io/ |
| 303 | +.. _`PSR-13`: http://www.php-fig.org/psr/psr-13/ |
0 commit comments