Prioritizing visible content for Google PageSpeed Insights made my responsive Jekyll website slower.

Serving up responsive images as fast as possible is at odds with how PageSpeed Insights evaluates prioritizing visible content

Derek McBurney
9 min readNov 14, 2017

Update as of December 2018 — Google has updated PageSpeed Insights and no longer punishes the technique described in this article (serving an embedded low-res image and lazyloading in a high fidelity asset, all while above the fold). My photography website covered in this article now scores 100 — so I guess the technique has been validated as a good one to use.

Google PageSpeed Insights is a valuable tool for determining speed related issues with a website. Google uses page speed as a factor in their search rankings, so checking off their list of optimizations is a wise idea to boost your SEO. Plus fast sites lower bounce rate and increase conversions — all that good stuff.

So as an exercise in achieving better Google PageSpeed Insight scores and learning a thing or two about the best ways to serve images responsively, I set out to update my old photography website. The site was designed with one philosophy for the portfolio pages: show the photo as large as possible. I built it as a response to all the bad photographer sites I saw where the photographer’s proudest work seemed to be given not much more real estate than a thumbnail image.

The site was built a long time ago and has gone through many iterations that you can trace through GitHub. Of all the sites I’ve worked on, this acts most as my playground — something I come back to and try something random and new with. It started as one of my early responsive websites. Then I updated it to be a Backbone JS website. Then it was later updated to use my Facebook photography page as a CMS to pull its images from. Then I added a Drupal blog to it. Eventually, I pivoted away from Drupal and Backbone to make it a back-to-basics Jekyll static site, for two reasons:

  1. Wrestling the control over quality and sizing of the photographs away from Facebook
  2. Saving money by removing any need for PHP and Drupal and using AWS S3 for hosting instead

Finally, my most recent goal became to serve optimal images for any device, and make Google PageSpeed Insights love me. I thought these goals would go hand in hand, but I learned that wasn’t the case.

Responsive Images with Grunt and Jekyll

When I began, my website didn’t have many photos and I manually executed an Adobe Photoshop batch job to resize my original photos down to a dimension I felt appropriate to burden all visitors of my site with.

Years later, with the guilt that my photography website wasn’t serving responsive photos, and now viewing the web through a new 5K iMac that made the large photos on my site now seem low resolution, I needed a better solution.

Before I could serve responsive imagery on the website, I had to generate it. Programmatically. Enter a task runner, Grunt. I dumped all the original high-quality photos into a directory in my project, then ran the grunt-responsive-images task against it. This allowed me to set what sizes I wanted my images saved at.

responsive_images: {
portfolio: {
options: {
sizes: [{
suffix: '.mobile',
rename: false,
width: 365,
height: 243
},{
suffix: '.mobile.2x',
rename: false,
width: 730,
height: 486
},{
suffix: '.tablet',
rename: false,
width: 719,
height: 479
},{
suffix: '.tablet.2x',
rename: false,
width: 1438,
height: 958
},{
suffix: '.desktop',
rename: false,
width: 946,
height: 631
},{
suffix: '.desktop.2x',
rename: false,
width: 1892,
height: 1262
},{
suffix: '.desktop-medium',
rename: false,
width: 1310,
height: 873
},{
suffix: '.desktop-medium.2x',
rename: false,
width: 2620,
height: 1746
},{
suffix: '.desktop-large',
rename: false,
width: 1758,
height: 1172
},{
suffix: '.desktop-large.2x',
rename: false,
width: 3516,
height: 2344
}]
},
files: [{
expand: true,
src: ['*.jpg'],
cwd: 'src/photos/portfolio/',
dest: 'resized-photos/portfolio/'
}]
},
}

The responsive images task offers quality settings, but I decided I wanted more control of my image compression, so I used grunt-contrib-imagemin with the imagemin-jpeg-recompress and imagemin-webp plugins to compress images into both JPEG and WEBP formats.

imagemin: {
photos: {
options: {
use: [jpegrecompress(
{
accurate: true,
progressive: true,
quality: "medium",
strip: true,
method: "smallfry"
}
)]
},
files: [{
expand: true,
cwd: 'resized-photos/',
src: ['**/*.jpg'],
dest: '_site/photos/',
filter: function (dest) {
var cwd = this.cwd, src = dest.replace(new RegExp('^' + cwd), '');
dest = grunt.task.current.data.files[0].dest;
return (!grunt.file.exists(dest + src));
}
}]
},
photosWebP: {
options: {
use: [webp()],
},
files: [{
expand: true,
cwd: 'resized-photos/',
src: ['**/*.jpg'],
dest: '_site/photos/',
ext: '.webp',
extDot: 'last',
filter: function (dest) {
var cwd = this.cwd, src = dest.replace(new RegExp('^' + cwd), '').replace(new RegExp('.jpg$'), '.webp');
dest = grunt.task.current.data.files[0].dest;
return (!grunt.file.exists(dest + src));
}
}]
}
}

A couple things of interest here:

  • The filter function is some logic to check if the compressed file already exists at the destination, and don’t run the compression again if this is the case. This allows for much speedier Grunt builds as only new photos I drop in require compression.
  • All this image handling takes place outside of the Jekyll build and is dumped into the compiled Jekyll site, with Jekyll instructed to ignore the directory the photos are dumped to. This means that updating the Jekyll site doesn’t blow away the responsive images that have been generated.

Since I’m generating assets that can reach up to 3516 x 2344 px in size depending on the device, I realize there’s one more crucial step to my responsive image delivery — tiny placeholder images that I can show immediately while the larger assets load in. The responsive image task in Grunt I’ve set to also create tiny 36px images that do get dropped into the Jekyll build directory so that Jekyll can embed them directly into its compiled HTML pages using a Base64 image encoder plugin.

With my images generated, it was time to figure out how to serve them through my website. Previously all I did was this in my Jekyll liquid markup:

<img src="{{ page.photo }}" alt="" />

Now my markup was this:

<picture id="focus">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop-large.webp, /photos/portfolio/{{ page.photo }}.desktop-large.2x.webp 2x" media="(min-width: 1920px)" type="image/webp">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop-medium.webp, /photos/portfolio/{{ page.photo }}.desktop-medium.2x.webp 2x" media="(min-width: 1400px)" type="image/webp">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop.webp, /photos/portfolio/{{ page.photo }}.desktop.2x.webp 2x" media="(min-width: 780px)" type="image/webp">
<source data-srcset="/photos/portfolio/{{ page.photo }}.tablet.webp, /photos/portfolio/{{ page.photo }}.tablet.2x.webp 2x" media="(min-width: 420px)" type="image/webp">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop-large.jpg, /photos/portfolio/{{ page.photo }}.desktop-large.2x.jpg 2x" media="(min-width: 1920px)" type="image/jpeg">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop-medium.jpg, /photos/portfolio/{{ page.photo }}.desktop-medium.2x.jpg 2x" media="(min-width: 1400px)" type="image/jpeg">
<source data-srcset="/photos/portfolio/{{ page.photo }}.desktop.jpg, /photos/portfolio/{{ page.photo }}.desktop.2x.jpg 2x" media="(min-width: 780px)" type="image/jpeg">
<source data-srcset="/photos/portfolio/{{ page.photo }}.tablet.jpg, /photos/portfolio/{{ page.photo }}.tablet.2x.jpg 2x" media="(min-width: 420px)" type="image/jpeg">
{% capture jpeg %}/photos-tiny/portfolio/{{ page.photo }}.tiny.jpg{% endcapture %}
<img src="{% base64 jpeg %}" data-srcset="/photos/portfolio/{{ page.photo }}.mobile.jpg, /photos/portfolio/{{ page.photo }}.mobile.2x.jpg 2x" alt="" class="lazyload" />
<noscript><img src="/photos/portfolio/{{ page.photo }}.desktop.jpg" alt="" /></noscript>
</picture>

This seems a lot more complicated, but it’s because I’m using a picture element to swap different images, using source elements to specify where those images are and when they show using srcset and media tags. This allows the browser to automatically serve up the right image based on my rules.

Now I’ve taken things one step further by using data-srcset instead of srcset, because I’ve opted to show the tiny embedded image first, and then lazy load in the full-size image with lazysizes. Lazysizes looks for the data-srcset tag and does its thing when it’s loaded the image. This allows a blurry version of the full-size photo to show immediately, and then the large version of the photo to load in when its ready. You can see that the tiny image gets shown in the img element using the Base64 Jekyll plugin mentioned above. I’m also using Picturefill to ensure the picture element behaves properly across older browsers. There’s also a noscript tag in there in the event a Luddite browses the site and lazysizes cannot execute, the regular desktop image size is served up (the size I was serving up by default before I implemented responsive images).

The result is showing the most optimal image size possible for every user, with perceived speed even when the browser needs to download a massive image over a megabyte in size.

This feels fast and looks gorgeous on 5K (but uh, not so much in an animated GIF). I’m happy with this.

Google PageSpeed Insights

With a solution for serving responsive images in place, I took a look at what I needed to do to meet Google PageSpeed Insights’ criteria. Mosts tasks were easy to accomplish, and the simplicity of this website aided in a few others.

  • Avoid landing page redirects — Responsive websites where one URL fits all solves this. Just keep the marketers at bay with their vanity domain names and this should never be an issue.
  • Eliminate render-blocking JavaScript and CSS in above-the-fold content — This task was easy due to the simplicity of the site — all CSS is required for above-the-fold content on this site since there aren’t any unique patterns that occur below the fold, so inlining all the CSS was all that was needed. The site doesn’t need JS to load with the page so referencing the javascript with the non-blocking async tag was sufficient.
  • Enable compression — I enabled this in AWS Cloudfront, which sits in front of my S3 bucket that serves up the website.
  • Leverage browser caching — My site doesn’t satisfy this criterion, because I haven’t found a way I’m satisfied with for programmatically setting cache headers on S3 files. I’d love a drop in file like .htaccess that could set rules based on file types, but there isn’t support for functionality like this. Please let me know how you solve this in the comments below.
  • Minifying CSS/JS — I do this out of the gate for all projects. In this particular site it’s accomplished with Grunt tasks.
  • Minifying HTML — This was easy to accomplish with Jekyll, there’s a cleverly executed Liquid template that outputs your layouts as minified HTML.
  • Optimize images — This was the goal of delivering compressed, responsive imagery. Despite my efforts, on a few subpages, PageSpeeds suggests further image optimization, but I don’t feel comfortable compressing the photos further and impacting visual quality.
  • Reduce server response time — This is up to your web hosting. AWS S3 through Cloudfront almost always comes in with a satisfactory server response time.

Prioritize visible content

Finally, the last box to check for Google PageSpeed Insights was prioritizing visible content. This is about ensuring that what shows above the fold is grabbed in the intial HTTP request. Unfortunately what I thought was a genius solution for serving up very large responsive imagery in a speedy way ran counter to what Google expected me to do to prioritize visible content.

“Only about 1% of the final above-the-fold content could be rendered with the full HTML response”

My implementation failed in the eyes of Google PageSpeed Insights. Google wasn’t happy about requiring a lazy load to grab the high-fidelity above-the-fold image. Despite how obnoxiously big the photo is in the layout, it isn’t 99% of the screen, so I’m not sure why Google says only 1% of the content renders in the HTML response, but it might as well kick me while I’m down.

So I tried removing lazysizes lazy loading, and no longer rendering a tiny version of the photo first, but rather just going straight to the full-size image (still responsively served up using the picture element). The result?

This feels… less good.

I got the Google PageSpeed Insights score I was looking for, but at what cost? This might satisfy the robots, but this is clearly a less optimal experience for a human.

Conclusion

Google is wrong. PageSpeed Insights is failing to recognize that the above-the-fold lazy load on my site is a progressive enhancement, and not required for page rendering. It’s analysis isn’t recognizing that images are served up out of the gate for above the fold content before the lazy load takes place — the base64 encoded data embedded images. Hopefully, they can correct this oversight.

My quest for a 100 score on Google PageSpeed Insights was fruitless, but I learned that numbers aren’t always everything. It remains to be seen what impact not ticking off all PageSpeed categories has on my site’s SEO, but my scores remain very healthy, and I favour perceived speed and a more seamless presentation for my site’s visitors.

Disagree? Solved this problem in another way? Let me know, and thanks for reading!

--

--

Derek McBurney

Head of Technology at Evans Hunt, sharing what I know about building the web for humans. https://dmcb.dev