Performance of srcset and TTFB

On a current project I’ve been troubleshooting a long time-to-first-byte (TTFB) issue and have narrowed it down to the srcset function.

For context, I’m looping through about 50 images and running this:

<?= $image->srcset([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800]); ?>

This is causing the TTFB for the page to grow by over 1 second. I’ve also discovered that if you repeat this line, the TTFB keeps growing linearly.

Is the srcset function expensive to run? I thought it would be a cheap lookup in the Media folder, but I’m not sure what happens under the hood.

Admittedly this is a lot of image sizes, but in this case it is justified due to the responsive design.

Has anyone had a similar experience? Is this expected behaviour?

Thanks,
Chris

Right now, the dimensions are read from each image file and used to calculate the correct final dimensions of the resized image. This is done to create a human-readable filename. Reading dimensions isn’t cheap when the files are large. We already have a fix for this, which will land in 3.2. Then the increase in TTFB should not be recognizable.

1 Like

You can already fix that right now with the following plugin. Add a images folder to site/plugins and put the following code into the index.php

<?php 

Kirby::plugin('faster/images', [
    'components' => [
        'file::version' => function (App $kirby, $file, array $options = []) {
            if ($file->isResizable() === false) {
                return $file;
            }

            // create url and root
            $mediaRoot = dirname($file->mediaRoot());
            $dst       = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}';
            $thumbRoot = (new Filename($file->root(), $dst, $options))->toString();
            $thumbName = basename($thumbRoot);
            $job       = $mediaRoot . '/.jobs/' . $thumbName . '.json';

            if (file_exists($thumbRoot) === false) {
                try {
                    Data::write($job, array_merge($options, [
                        'filename' => $file->filename()
                    ]));
                } catch (Throwable $e) {
                    return $file;
                }
            }

            return new FileVersion([
                'modifications' => $options,
                'original'      => $file,
                'root'          => $thumbRoot,
                'url'           => dirname($file->mediaUrl()) . '/' . $thumbName,
            ]);
        },

    ]
]);

That’s the component code we are using for 3.2.

One thing you should be aware of though. Srcset should be used carefully. If you have too many steps, the optimization is no longer really useful. The resulting file size between 1200 and 1400 will not be massively different most of the time, but your users will likely have to load multiple files when they resize the browser only slightly. Your server also has a lot more work in the end. Always check if the steps really make sense.

1 Like

Thanks @bastianallgeier, I look forward to the 3.2 release!

You raise a good point about the file size difference between images and I’ll review this. My initial instinct is to give the browser as many options as possible and let it decide what to do. However as you rightly point out, if a user has to load a second image almost the same as the first, srcset may well be more expensive than just loading the full image initially through src.


The code you send through isn’t working for me in 3.1.3, I’m getting this error:

TypeError thrown with message “Argument 1 passed to Kirby\Cms\App::{closure}() must be an instance of App, instance of Kirby\Cms\App given, called in […]/kirby/src/Cms/FileModifications.php on line 158”

Stacktrace:
#20 TypeError in […]/site/plugins/images/index.php:5

Try this:

<?php

Kirby::plugin('faster/images', [
    'components' => [
        'file::version' => function (Kirby\Cms\App $kirby, $file, array $options = []) {
            if ($file->isResizable() === false) {
                return $file;
            }

            // create url and root
            $mediaRoot = dirname($file->mediaRoot());
            $dst = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}';
            $thumbRoot = (new Kirby\Cms\Filename($file->root(), $dst, $options))->toString();
            $thumbName = basename($thumbRoot);
            $job = $mediaRoot . '/.jobs/' . $thumbName . '.json';

            if (file_exists($thumbRoot) === false) {
                try {
                    Data::write($job, array_merge($options, [
                        'filename' => $file->filename()
                    ]));
                } catch (Throwable $e) {
                    return $file;
                }
            }

            return new Kirby\Cms\FileVersion([
                'modifications' => $options,
                'original' => $file,
                'root' => $thumbRoot,
                'url' => dirname($file->mediaUrl()) . '/' . $thumbName
            ]);
        }
    ]
]);

It’s the same code as above with added namespaces.

Ah, sorry! Nils is right. The namespace is missing. I took it 100% from our 3.2 implementation without double-checking.

Bastian, out of interest, what’s the reason why Data::write doesn’t have to be prefixed with the namespace?

There’s a class alias for the most common classes. Kirby has one too. So you could also write

function (Kirby $kirby, $file, array $options = []) {

}

instead

1 Like

Thanks :smiley:

Thanks Nils, I’m not getting any errors now.