Ben Oliver

Now

Reserving space for images in Jekyll

Avoid that sudden jump in your layout when images load.
22 December 2020

This website has banner images on a lot of posts. It sucks when you load the page, start reading, then the image comes in and bumps everything down.

This is sometimes called Cumulative Layout Shift, or CLS. It is really crap for mobile users in particular.

You can completely negate this by reserving the space for your image in advance, while it loads. To do this, you must know the size or aspect ratio of your image, then draw a box that size. There’s a great jekyll plugin to help you with this called jekyll-image-size1.

I’m going to work with aspect ratio here, instead of width in pixels. Shape, rather than size.

I would highly recommend reading this article from CSS-Tricks2 on how to draw the boxes. I am borrowing a lot from them in this post, before I then steer us on to Jekyll.

The key thing is to remember that your width is always 100%, it’s the height that matters. So say you have an image that is 2000wx1000h. The width of your box is 100%. So your box would be 100% width, 50% height.

jekyll-image-size can work out the height percentage you need, right in your template. You just give it an image (you can use liquid tags) and it’ll spit out a value:

{% imagesize example.jpg:height?width=100.0 %}

This just prints a single integer. In the example of a 2000x1000 image, it would print out 50.

Then you need to use this in your CSS. You probably will end up using inline <style> tags so you can render a different value for different images.

Stolen straight from the article:

<style>
.aspect-ratio-box {
  height: 0;
  overflow: hidden;
  padding-top: {% imagesize example.jpg:height?width=100.0 %}%;
  background: white;
  position: relative;
}
.aspect-ratio-box-inside {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
</style>

So your result gets used in padding-top, with a % added to the end.

Notice that you need two divs for this to work, one within the other. You then just put your <img> inside the box-inside div and hey presto.

And the Bonus ball - how to get this to work with a Content Security Policy

This is where it gets painful. What if you have a strict CSP like me that only allows either local CSS files or hashed values? You can’t use a nonce because this is a static site.

You don’t know what that piece of CSS in your <style> tags is going to be until you get the image loaded up. The padding-top will always be different.

Well, I just generated a CSP hash for every value from 1 to 100. In a landscape (or square) image, the height will never be more than the width, so the padding-top will never be more than 100. It’s hacky, and won’t work with portrait images, but for me on these banners it works great. You could easily come up with some extra logic to handle portrait images.

Below is the list of all of them, so you can use them too.

I should say that the string that is hashed is the following:

.outside-placeholder{padding-top:X%}

Where X is a number from 1 to 100. No semicolon. So the hash for:

.outside-placeholder{padding-top:36%}

is

'sha256-Hvg9h1/0+83zIO8mYiJmJv9pQGkNryyP+Cp8vkiAOGc=' 

I have put the rest of the CSS from the example in my external CSS file to keep it simple, I just hash the part that is always changing.

Hopefully this might be of use to anyone trying to do the same thing!

Here is the list:

'sha256-NBAisHSdOayPeJgUU1m19FRFx9bSWx02u0GROV7h/8E=' 
'sha256-AQ7cPBUGJBnxmiiJ/mikze3hBKY8K8ckFyLWDD0KdoI=' 
'sha256-n5JcUMNr57vsOjPATGEnpepgB75R/mxPo8tMrxrjfAM=' 
'sha256-AYCNrwsL9FG7jqoyglY5m+kqBgKHTb2AOfRM/NUqxPY=' 
'sha256-x2SZ4cnkfa4j9QYvl4P4y5uKcr/E7O2e8TM7kc1znDI=' 
'sha256-Dv8k+Xtob3saDRxD+KTnEukvJ6jFJ9Q3HTyqYsWUqaQ=' 
'sha256-vr2J+yzM/TCy1bgDqnAdzFH8UZv2mv511mUSBUUeKtE=' 
'sha256-P4pKwrzv6HKOZ5j/DzzxUN3hY9x6Z2RiBwAs1Z9ouS8=' 
'sha256-2zf0pVT8CEF4HkdmL7+03VonrzwCkCIwqLdLcE+11QM=' 
'sha256-112riUOCJ07yhzd0dPsAoPOp2HdRF6fLTfG2E4R+6F0=' 
'sha256-SxAWt5Vfx5Q20KSJ9mg7Ej0hOE0QPZEVHg7va6pBtCM=' 
'sha256-5oqIv+RCjdfpkUnxk8UD5hVTZIQ84JIM6rmwjdMEd5I=' 
'sha256-T2cKnk4kEYGrRbYfAFDMHd0UpGn6S6oGacX+m/XXHRM=' 
'sha256-16Bqh71vPtS2KXg+5o/Nn4tBdiGcYe8BbS/UpTMubF8=' 
'sha256-KCH4lsq/lSiqXpSHdFFRb8JB8Joorqm8UWW8tMQobnI=' 
'sha256-GoC0lU1JwWvAlcf+/uJhb7Eh2M2+clNgG0QkXT5nkkc=' 
'sha256-e9jb0C+tJZ2Snzp1vmWdprCWE1jxG6nI22acYObTeaA=' 
'sha256-IfeWxjSuSn10IHIwhyE0WCOZn18hkNc+GXQn9c6glaM=' 
'sha256-QoaD+ceM4OQshNeA+2esqYEORtkhw66FsluRG9CTIhM=' 
'sha256-EeVrS1+RqAHBk+QivgqHWOpXQzK934eHt1AIRTVFjJQ=' 
'sha256-Xf+rQinKGNTRHl7Fb0YNQD6g3NuqaYZ5D5ynteqktaU=' 
'sha256-t03xoVnGvhHARyQIFKcvOE8mgCXn9d7EQysBq2h83QM=' 
'sha256-xKmNhXbg9kqBzEgn+DJA3Mm/xHSjMXxV7lgLZqNy+iM=' 
'sha256-EudU+cy23xyBWEUHQ0dydf50bKWrdFMYoeHIuk2ejqI=' 
'sha256-jVJVzKFTJmJf4kcOTbT2zxhSXatbDHthkcA4iZ+rEOk=' 
'sha256-poF0EA4705HqiXE4ocY06D7mn8aV+GAGQsw86y8/8CY=' 
'sha256-eNs0MCJajSFEslEx7seOwqXSZDv23fJZYq49Oeo4cRY=' 
'sha256-kcEcx3VPIkddYL8QACfmqEaQcAosTw0Piq3uN0mUQ6A=' 
'sha256-8ljobqt3/igVq2MTWwQipFl3X8y/qm0M3JEy+uH97gI=' 
'sha256-kXn1gECbHEJHICNUufOA2AK4pMueQ9eeDay5RWyCF1I=' 
'sha256-lr6oSfZXtc5Izsi1E6UqMWbbKjyCUYrxzVy8JpMCMpE=' 
'sha256-vLV2T3uh/xdRiju20fLkBpZR4zYN4STTdr/HceL2VuY=' 
'sha256-mb6fkYpCJSYPPY3wgZdjo0Y1HYRQf2kZ2AGWmnr3Dq4=' 
'sha256-NdIUadtrWcQHdyqdfzaAuSacovWL8E/wxN2eRF6b3xg=' 
'sha256-GwPuRlx7u3Vz52FySEPOit/KzDHnBVxA0UTjq1ROkKo=' 
'sha256-Hvg9h1/0+83zIO8mYiJmJv9pQGkNryyP+Cp8vkiAOGc=' 
'sha256-Myny8+CGSkYaj57HBwMYsTtOeP9nHX6agaEGgOzQ1qQ=' 
'sha256-SBGzvgLpxzwTOCWGq7NwFCRc4YQFLdc6NaV51BmwiiE=' 
'sha256-ufdF1jcFBrfQVYagCUw2FH3eIx9/NLQPNBZzKBueZTE=' 
'sha256-RgCX9gsoZVhB8yxoX0/9Ia4qF0drIlKHBjkNObtbUNQ=' 
'sha256-sWX+r0Nk0Bk0IaXmzaH3XRfmnd6D6FxJ28GKNhofWmA=' 
'sha256-jtYAlTg0UbXryxS9YLH9AKhA+Zjxpz3UpQ9TrTd4L80=' 
'sha256-EPJgMwIqbAIo0TJoGoDojkY5YS1HdmIhJeHPpQ83heM=' 
'sha256-PR9M0J2KDmvTmL/PzAa4ydFrZDmcj+JqWjcSj9SFbQw=' 
'sha256-C2UC43k8GUY1vDlY4j1QTa3XccE6RHexBppiESvjRWk=' 
'sha256-LQeDaS525XBxFBeTLN7hIo7dUVhMyWOFLUSS2QTeRsU=' 
'sha256-RBDGRbC2XemfergCfCHvBLSUgmklIk4yaoKgBLKhkWE=' 
'sha256-xTG0AT06JpE05zMgfWCTmJ5CR6z0VrLBsf/4z/dqmgk=' 
'sha256-zj2HrlPxrAyC6OuqhauuAGQnXdziOVUXAK9CpWUnWzs=' 
'sha256-QNQsKE/GGmQaz5lwA3BCz5dNWlmZynmd50l41zTpH4A=' 
'sha256-LodaHza69pycPckU/AcWhhX6dznu+BDKKaGswMelUwc=' 
'sha256-fDH4Md+kinrqXINnilkOK8yvC9VaZoFHbL8ydpCNdwI=' 
'sha256-xpR7ALMGYAQdZfivWn3CVhRHSbmlqY4GlSS1dIfOy5c=' 
'sha256-qoy1/x3Oi33GSuYgAWpdbywmaDJtwfKjmBIltNy14cE=' 
'sha256-x0zBcDfU2bgK0IG0Nc5d4sUVEY6No2Zqa1FZZl9ZSqM=' 
'sha256-4nz5QU8/J+CfLEdcTtdjSzNjz0g8l1E6OlAnsXIrEc0=' 
'sha256-4khToUvtO7hpaRACxcl9RP8fyGgQioBAMr68XhjG0Vo=' 
'sha256-R5lQ8twaG7d/t/tamj1h85XgUbmjs0YmVuarEd2UIq4=' 
'sha256-06FqQtk1iQGOm94QmIdJZ6PAPXLePlchH8E4aczGTz8=' 
'sha256-uWvlBMXbcHEQp/VXAP50XvDaBBfAYA9xk3b7b44io9g=' 
'sha256-/22DDhXW7Kf0TCVnQxVwmEqwz9Ul68Uo6LL5A5Mf7j0=' 
'sha256-nflGwWMT3A+KUnuc+8w58hNZJcdaIv3JMryee5d2SGE=' 
'sha256-nuxGq1v8nRlfoO8NKE5NcBVx66tq8O9dIRUVNdM0I5c=' 
'sha256-b6Vbkh8ZhX0sqK/0RNBeAJpF5tmz+RRzmUmQIVp0v1A=' 
'sha256-q1qSbNtHqg1sTMCymam3/X86Tsx/ew9Pn79bUvcrt+0=' 
'sha256-+p1opjBvCEIyxw9UPh3qXUXQopvNAjNACk/DPpzVrbo=' 
'sha256-Bw3oTqCE9xckyvEnkxMojX9h/QrtWtDsMSSF+Af3fgQ=' 
'sha256-ea/HZmOfPzyYupwCo4BNRHr0M69mFp2WAQl+qIFOEB8=' 
'sha256-/rB31fjUdU9lhnG4ZjlDfM1LNd63ZhLzXxYNB2c6giU=' 
'sha256-c+9cdBqcnQcoyaL/NqBTyih1f4cxRynEO2vf6Fzdc4w=' 
'sha256-nts0iQL4VIx9Cwq0TZmcn1W77RodcYsb5OnxXoC/9BY=' 
'sha256-tgPYaU/HOLOA4YizDphBBA3M6yD6azDrbR7C9v3K3xA=' 
'sha256-DgaisoQSTiXecXBuNXvaXkjvxjMYUASi5uxr0UhZa4M=' 
'sha256-4iY5ZKVMap1tQZIDYmYJwVWb1eHS343ry58gTvRYFV8=' 
'sha256-E6VufUszXi0qTQZPxyAQIE3updHBYNvI60BsnRWDKns=' 
'sha256-CgnlI+J8MEXsRj5UmlL4J0nRK0l76/q4tABIOkWqDOY=' 
'sha256-M6QmZKaRFl3vDXoIGC3Zxuq5yfH+epzwophqtqYWh58=' 
'sha256-1uN68+OxnVuH+2ecZ8ukz2nl6bjl+XeNEQVUgPS4p8Y=' 
'sha256-ar8lZsFlb5yaEHWwYUFIn847AnyEr3GWg3b+uvPC21k=' 
'sha256-J8wp/eEPzfrgnaS6Mwalt11XRlc7S45Xsh2mktl3gtY=' 
'sha256-aXkaxTC4ErFHOEng9CCPs6HcR2EscHtZBeZni1HmxtA=' 
'sha256-XLGeh6E7W3DCeCYLsL1l6cL1IGtcde+JPehbNd0/pxE=' 
'sha256-IJQzi8IWMNkk3cQM8Ibg1JtKUN02E7oVQisGW+o7lcQ=' 
'sha256-O44kpOWVC8F+Tmjpw9xCX+qx7dGt9mvqR2vpEWo5TpM=' 
'sha256-kW4SZOyGXLKx6hfm15Y1qz17fhvixR9y+P5334V+ANU=' 
'sha256-nZh/TwuyTF1FXMArGd2J5UG9yq8gxTyhG4VEF5vfEHA=' 
'sha256-/N2hT3MIZIHVhxP34m8W1n6fwnW92TgE2BgJSRaTyu4=' 
'sha256-fRFev9ofikSNdXJ59AshnIt3Q2t/jXVmGY2WMBD7j5U=' 
'sha256-m6oY78p4E3Jv31xeRzOe48qS0+XukzIkNkSbTYnF4oE=' 
'sha256-sEqss1ktX15lw4Q5LTUhV2ERTOfnqIR4QGKOno1PiL0=' 
'sha256-JZrvV32LMm5x4uIj9sAdiCrkwQPulNUaWN2NQLZbZ0I=' 
'sha256-hx9X5TGzxYG6R3rN9GTDu7zBe6QSl62c32sseFKOxhQ=' 
'sha256-q/3nEaYynuuJRjkDexHSNkHQDJyvLxP1GnLOjkwwQW8=' 
'sha256-p9ZBvfnWGrSrBjhsJG/4R6poKijIxIGmCKs3pGt0gfo=' 
'sha256-sCG6uUhbUTnr/7tQh0pIxIqXUO+CcMbYqfilzLdyX5o=' 
'sha256-ThGT3Aal2SvdVuYdRYJH9VJRkXxYJ9VCs4FvZxS8mto=' 
'sha256-igGu568KM2kREae9tLWV+x27lWDCim7FbkdL4O32SkM=' 
'sha256-YhrkPuD7m4kyofXwYp8KuXzPLoP7jt/e8dVewW/tQO0=' 
'sha256-Oz+YesySyxwWjPD0GgLNfymiC2sEVT4TvZJUavmuhH0=' 
'sha256-9Rx4V1f0hCDMpltDZfbMCrUZGaVp8/F/FOpPY5kfiqI='