Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Optimizing & Profiling

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Instead of me telling you the site is fast, let's prove it! In Chrome, there's a tool called "Lighthouse", which you can also get for some other browsers. Run this for just performance and select "Analyze page load".

This is the best way to see if you have any frontend performance problems. Our score will likely be pretty high - simply because our site is small and quick - but we can use the report to zero-in on a few possible problems. And... yep! We got a 98 with no build system! That's amazing. But we can do even better.

Eliminate Render-Blocking Resources

If we scroll down, we can see where our problem areas are. The first is "Eliminate render-blocking resources", which points to our font file. A lot of what we're going to talk about has nothing to do with AssetMapper: it's just frontend performance in general. If you open templates/base.html.twig, we have a <link> tag that points to this font file.

... lines 1 - 2
... lines 4 - 11
{% block stylesheets %}
... line 13
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/inter@5.0.3/index.min.css">
... line 15
{% endblock %}
... lines 17 - 21
... lines 23 - 72

When your site sees a ` tag, it downloads it before it renders the page. So it basically freezes the rendering of the page until the download finishes.

But this is interesting. Open that file... and let's get a non-minified version. It has a bunch of potential font faces. Here's how this works: our browser downloads this file immediately... but the font files themselves won't be downloaded until and unless we use this font. Additionally, font-display: swap tells the browser:

Hey, it's ok to render some text that's supposed to use this font, even if the font isn't downloaded yet. You can use the default system font first, show the text, finish downloading this font file, and then use it.

Essentially, this CSS file is written in a way where all of these font files are going to be downloaded lazily. The problem, which isn't really a big problem, is that, at this point, our browser just sees a CSS file and thinks:

I need to download that CSS file right now and I can't render the page until that finishes!

Once it does finally see the CSS contents, it discovers that there are a bunch of font files that it can lazily download.

So, CSS files are render-blocking resources... which is normally great, because we don't want the page to render unstyled for a half second before the CSS downloads. But this particular file is funny because it is a "render-blocking" resource... but doesn't contain anything critical.

If we care enough to eliminate this render-blocking resource, we can move it into app.css. Start by copying this file... or really just the font faces we need: a lot of these are for languages that we're not using. I'll copy the two Latin fonts... though we likely don't even need this Latin extension one. Then delete this CSS file entirely, go to assets/styles/app.css, and paste. These aren't real URLs... so go copy the URL... paste, take off the index.css... and that should be fine. Copy the URL again... and do the same thing down here.

... lines 1 - 4
/* inter-latin-ext-wght-normal */
@font-face {
font-family: 'Inter Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(https://cdn.jsdelivr.net/npm/@fontsource-variable/inter@5.0.3/files/inter-latin-ext-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0100-02AF,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;
/* inter-latin-wght-normal */
@font-face {
font-family: 'Inter Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(https://cdn.jsdelivr.net/npm/@fontsource-variable/inter@5.0.3/files/inter-latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
... lines 24 - 37

Perfect. This is adding some complexity to our code for only a small gain, so I'd say this is a lower priority. We have basically the same amount of CSS as before, but we've eliminated a small, unnecessary blocking resource.

The other failure we have is similar. It's for FontAwesome - specifically, this JavaScript file. That's also in base.html.twig. Since this <script> tag doesn't have defer or async on it, this will also block the rendering of the page. If we want, we can add defer to this, which says:

Start download this immediately, but don't block the page while it's finishing.

... lines 1 - 2
... lines 4 - 16
{% block javascripts %}
... line 18
<script defer src="https://kit.fontawesome.com/5a377fab5b.js" crossorigin="anonymous"></script>
{% endblock %}
... lines 22 - 71

Because this is for FontAwesome fonts, the worst-case scenario is that the page loads and then our font icons show up just a moment later.

Profiling Again!

Okay, now that we've changed a couple of things, let's test it. To save time redeploying, I'll go back to my local site and run Lighthouse again. "Analyze page load"... make this a bit bigger, and... awesome! We're getting 100 locally!

But if you look down here... we do still have some opportunities to improve. We see "Serve images in next gen formats", which is a good thing to check on later, but not related to Symfony or AssetMapper. This "Avoid serving legacy JavaScript to modern browsers" - I believe that's referring to the importmap shim: the code that makes the importmap work on all browsers. That's small & necessary, so not a big deal.

Avoiding Chaining Critical Requests

But below that, we see "Avoid chaining critical requests". This is probably the most important item on this list.

Here's what's happening. As you can see, it downloads the HTML first. Once it does, it realizes that it needs to download this CSS file. Once it downloads the CSS file, it realizes that it needs to download this font file. See the problem? Instead of knowing - from the start - that it needs these and downloading them all at once in parallel, our browser is finding out about them little by little. Ultimately, it means this font file will take longer to load because it can't start downloading it until it downloads a few other files.

How can we fix this? Preloading. Let's talk about this important topic next.

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^4.0", // v4.2.0
        "doctrine/doctrine-bundle": "^2.7", // 2.10.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
        "doctrine/orm": "^2.12", // 2.15.2
        "knplabs/knp-time-bundle": "^1.18", // v1.20.0
        "pagerfanta/doctrine-orm-adapter": "^4.0", // v4.1.0
        "pagerfanta/twig": "^4.0", // v4.1.0
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.1
        "symfony/asset": "6.3.*", // v6.3.0
        "symfony/asset-mapper": "6.3.*", // v6.3.0
        "symfony/console": "6.3.*", // v6.3.0
        "symfony/dotenv": "6.3.*", // v6.3.0
        "symfony/flex": "^2", // v2.3.1
        "symfony/framework-bundle": "6.3.*", // v6.3.0
        "symfony/http-client": "6.3.*", // v6.3.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.3.*", // v6.3.0
        "symfony/runtime": "6.3.*", // v6.3.0
        "symfony/stimulus-bundle": "^2.9", // v2.9.1
        "symfony/twig-bundle": "6.3.*", // v6.3.0
        "symfony/ux-turbo": "^2.9", // v2.9.1
        "symfony/web-link": "6.3.*", // v6.3.0
        "symfony/yaml": "6.3.*", // v6.3.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.6.1
        "twig/twig": "^2.12|^3.0" // v3.6.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/debug-bundle": "6.3.*", // v6.3.0
        "symfony/maker-bundle": "^1.41", // v1.49.0
        "symfony/stopwatch": "6.3.*", // v6.3.0
        "symfony/web-profiler-bundle": "6.3.*", // v6.3.0
        "zenstruck/foundry": "^1.21" // v1.33.0