Zach’s ugly mug (his face) Zach Leatherman

Don’t Shut Down Your Business! Instead Use Eleventy Image

January 25, 2021

This talk was first delivered for Jamstack Toronto.

Hello, Zach Leatherman here, Local SSG Owner and 👍🏻 Image 👍🏻 Enthusiast 👍🏻. Are your JavaScripts making too much noise all the time?

Well image optimization can’t solve that problem but I am here to tell you about Eleventy 🖼 Image—an exciting new utility to perform build-time image transformations for both vector and raster images. It’s easy to use and it does not require Eleventy. You can use it in any Node.js script.

Contents

The Hello World

First, npm install @11ty/eleventy-img into your project.

Then head over to Unsplash and find yourself a nice image. I like to look for a nice picture of a Nebula. Here’s a good Nebula. Download a copy of this image locally to our project directory and name it nebula.jpg.

Make another file in our project directory called demo.js and use the following contents:

const Image = require("@11ty/eleventy-img");

(async () => {
  let stats = await Image("nebula.jpg");

  console.log( stats );
})();

Run this script using node demo.js and you’ll see something like this:

~/Code/jamstack-toronto ᐅ node demo.js
{
  webp: [
    {
      format: 'webp',
      width: 7857,
      height: 7462,
      filename: '7fc15c7-7857.webp',
      outputPath: 'img/7fc15c7-7857.webp',
      url: '/img/7fc15c7-7857.webp',
      sourceType: 'image/webp',
      srcset: '/img/7fc15c7-7857.webp 7857w',
      size: 3439498
    }
  ],
  jpeg: [
    {
      format: 'jpeg',
      width: 7857,
      height: 7462,
      filename: '7fc15c7-7857.jpeg',
      outputPath: 'img/7fc15c7-7857.jpeg',
      url: '/img/7fc15c7-7857.jpeg',
      sourceType: 'image/jpeg',
      srcset: '/img/7fc15c7-7857.jpeg 7857w',
      size: 6245602
    }
  ]
}

By default, Eleventy Image creates a webp and jpeg version of the image you feed it, with the same dimensions. It looks like we fed it a very large image, 7857×7462 and a ~3MB webp plus a ~6MB jpeg.

If you want to create different formats, you can change this behavior using the formats option. It looks like this:

 const Image = require("@11ty/eleventy-img");

 (async () => {
   let stats = await Image("nebula.jpg", {
     // ["webp", "jpeg"] is the default
+    formats: ["jpeg"],
   });
 })();

You can use jpeg, png, webp, avif (new!), and svg (although SVG requires an SVG input file).

Remote Control

If you return to our lovely Nebula image on Unsplash, we can modify our script to download this remote image for us.

This is especially useful when your content is being driven from a CMS or other external data source. We want to optimize those images too.

Right click on our Nebula and retrieve the big raw image URL. You may want to remove the w= and q= parameter to get as close to the raw original as possible.

Instead of nebula.jpg, let’s use this URL.

const Image = require("@11ty/eleventy-img");

(async () => {
  let stats = await Image("https://images.unsplash.com/photo-1462331940025-496dfbfc7564?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop", {
    formats: ["jpeg"]
  });

  console.log( stats );
})();

which outputs:

~/Code/jamstack-toronto ᐅ node demo.js
{
  jpeg: [
    {
      format: 'jpeg',
      width: 7857,
      height: 7462,
      filename: '2056cbb-7857.jpeg',
      outputPath: 'img/2056cbb-7857.jpeg',
      url: '/img/2056cbb-7857.jpeg',
      sourceType: 'image/jpeg',
      srcset: '/img/2056cbb-7857.jpeg 7857w',
      size: 6245511
    }
  ]
}

This is awesome because the request to the remote URL is cached (by default) for one day. No additional network requests are made. No source images are needed to be stored in your repo. You can work offline (even after the full day has passed) and it will use this cached copy. (Check out the additional Caching options)

I use this for all of the avatars on the 11ty.dev. The home page alone includes 522 separate images and still weighs only 852 KB.

Going back to our nebula image—at over 6MB—it’s obviously way too big.

Travel Size

Let’s resize our Nebula to 1400px.

 const Image = require("@11ty/eleventy-img");

 (async () => {
   let stats = await Image("https://images.unsplash.com/photo-1462331940025-496dfbfc7564?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop", {
     formats: ["jpeg"],
+    widths: [1400], // by default this uses the original size
   });

   console.log( stats );
 })();

which outputs:

 ~/Code/jamstack-toronto ᐅ node demo.js
 {
   jpeg: [
     {
       format: 'jpeg',
+      width: 1400,
       height: 1329,
       filename: '2056cbb-1400.jpeg',
       outputPath: 'img/2056cbb-1400.jpeg',
       url: '/img/2056cbb-1400.jpeg',
       sourceType: 'image/jpeg',
       srcset: '/img/2056cbb-1400.jpeg 1400w',
+      size: 170827
     }
   ]
 }

A much more reasonable 170KB.

Please don’t make me write HTML

If you don’t want to write the HTML, Eleventy Image can do that for you too.

 const Image = require("@11ty/eleventy-img");

 (async () => {
   let stats = await Image("https://images.unsplash.com/photo-1462331940025-496dfbfc7564?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop", {
     formats: ["jpeg"],
     widths: [1400],
   });

+  console.log( Image.generateHTML(stats, {
+    alt: "A bomb ass nebula",
+  }) );
 })();

Outputs:

~/Code/jamstack-toronto ᐅ node demo.js
<img src="/img/2056cbb-1400.jpeg" width="1400" height="1329" alt="A bomb ass nebula">

What’s awesome here is that we get the width and height attributes of our output image added for free. This allows the browser to set the aspect ratio of the image while it’s loading to avoid Content Layout Shifts.

The second argument to generateHTML is any HTML attributes you’d like to include on the <img>. Make sure you include alt or we’ll throw an error! alt="" works fine.

It gets better. Let’s feed it a more complicated stats object, with more formats and more widths:

 const Image = require("@11ty/eleventy-img");

 (async () => {
   let stats = await Image("https://images.unsplash.com/photo-1462331940025-496dfbfc7564?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop", {
+    formats: ["avif", "webp", "jpeg"],
+    widths: [600, 1200, 1800],
   });

   console.log( Image.generateHTML(stats, {
     alt: "A bomb ass nebula",
+    loading: "lazy",
+    decoding: "async",
   }) );
 })();

Outputs:

~/Code/jamstack-toronto ᐅ node demo.js
<picture>
  <source type="image/avif" srcset="/img/2056cbb-600.avif 600w, /img/2056cbb-1200.avif 1200w, /img/2056cbb-1800.avif 1800w">
  <source type="image/webp" srcset="/img/2056cbb-600.webp 600w, /img/2056cbb-1200.webp 1200w, /img/2056cbb-1800.webp 1800w">
  <source type="image/jpeg" srcset="/img/2056cbb-600.jpeg 600w, /img/2056cbb-1200.jpeg 1200w, /img/2056cbb-1800.jpeg 1800w">
  <img src="/img/2056cbb-600.jpeg" width="600" height="569" alt="A bomb ass nebula" loading="lazy" decoding="async">
</picture>

We just generated 9 different images (3 formats, 3 widths) and the generateHTML was smart and used <picture> instead of <img>. Hooray!

Next try passing a sizes attribute to your generateHTML call.

SVG Flies First Class

It’s easy to do vector to raster conversion, too. This would likely work great for programmatically generating OpenGraph images, for example.

 const Image = require("@11ty/eleventy-img");

 (async () => {
+  let stats = await Image("https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg", {
     formats: ["avif"],
     widths: [1200],
   });

   console.log( stats );
 })();

Outputs:

~/Code/jamstack-toronto ᐅ node demo-svg.js
{
  avif: [
    {
      format: 'avif',
      width: 1200,
      height: 1200,
      filename: '11efb293-1200.avif',
      outputPath: 'img/11efb293-1200.avif',
      url: '/img/11efb293-1200.avif',
      sourceType: 'image/avif',
      srcset: '/img/11efb293-1200.avif 1200w',
      size: 53041
    }
  ]
}

You can passthrough svg input to svg output too, which may seem strange. But when you’re using remote images, perhaps from a CMS, and you don’t know what image format the end user might be uploading, it’s useful to preserve the vector format if one is available! See also the svgShortCircuit option.

CSS Background Check

Note that this is the first example so far that uses Eleventy to function.

Using Eleventy Image with a CSS background image is only slightly more tricky. Consider the following Eleventy template (perhaps named demo.11ty.js) that outputs a CSS file:

const Image = require("@11ty/eleventy-img");

module.exports.data = async function() {
  return {
    permalink: "/style.css"
  };
};

module.exports.render = async function () {
  let stats = await Image("nebula.jpg", {
    formats: ["jpeg"],
    widths: [600],
  });

  return `#hero-div {
    background-image: url(${stats.jpeg[0].url});
  }`;
};

You might image any number of things that Eleventy Image could be used for: Favicons, Open Graph images, raster images embedded inside of SVG using <image/>.

In addition to local file names and remote URL strings, the first argument to Image() could be a Buffer too—maybe even from Puppeteer’s page.screenshot().

Go forth

Image optimization won’t mitigate your JavaScript performance costs, but it can make your page load faster and lighter! Eleventy Image can help.


< Newer
Barebones CSS for Fluid Images
Older >
video-radio-star Web Component

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at IndieWeb Avatar for https://cloudcannon.com/CloudCannon. He is the creator and maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 79 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

22 Reposts

EleventyMatt DeCampAndy BellNolan FranklinJean Pierre KolbLoad LabzRaymond Camden 🥑Rares PortanRyan No Seriously Wear a Mask BooneTristan GibbsMatthias OttThomas AllmerMohammad ArifNathalieJulien BrionnePatrick FaramazIndieWeb Avatar for https://martinschneider.meEleventyJamstackTORONTO aka Jamstack U.N. 🇺🇳IndieWeb Avatar for http://54.252.222.218IndieWeb Avatar for https://www.pixellyft.comIndieWeb Avatar for https://www.zachleat.com

127 Likes

MaëligRhian van Esch⚙️ Christian SharafBrett JankordNaomi See 🤖👩🏻‍🎤❤️🤦‍♀️🥸🦠Patrick HaugNixinovaMike AparicioDanny de VriesJasper Moelker 🇪🇺💜⚡CosCarles MuiñosRobert McCrearyPeter AntoniusAndrew AquinoMarco useCauseAndEffect()Paul ApplegateDave 👾 Working from homeDennisAlena Nikolaevaaaron hansDevessierBrett EvansEleventyRattleknife DesignChris TseStephanie EcklesTrevor TwiningOleksandr ShutLee FisherMaëligNathalieDanny de VriesDinisCarol 🌻okdiosDrew AmatoGuillaume DeblockAnvesh ⚛️🌐Michelle BarkerDavid Hund ✌Mia || MiriamRobin RendleWebchefz InfotechDanielTodd 🦞Amber WilsonJohn MeyerhoferKarim JordanVladislav ShkodinBrett EvansMathias Rando JuulJosh CrainJorge del CasarAlex@home 🏡 🌻✊StackroleAndrew ColdenThomas HollandThomas AllmerKevin Gimbel 🖖 (he/him)Iago Barreirodan leathermanJan KollarsᴅᴇʀᴇᴋStephan JägernlbEl Perro Negro 💙💛🇪🇦Jan SkovgaardMichael GehrmannZeno RochaAnders GrendstadbakkMatt BiilmannJonathan YeongMatt Tunney ☕️✨JoëlRyan No Seriously Wear a Mask BooneVictor CamnerinCallum GrantHugh HaworthMarc Littlemore 🤖CAHO badMartin Berglundzack - building tools for NotionMichael HastrichStefan FeserSalmndrNolan FranklinThis Dot LabsCody Peterson #BLM🍄 Bobby 🍄Jean Pierre Kolbbudparr, enthusiastT Carter BaxterPaul ApplegateAlex ClappertonErik VorhesFluxmod - Email MarketerDevin ClarkAlejandro RodríguezTanner DolbyLoad LabzChristian | 👨🏼‍💻Uncle AniekanJack 🕺Jens GrochtdreisMaxime Richard_octoJack𝕕𝕘𝕣𝕒𝕞𝕞𝕒𝕥𝕚𝕜𝕠Matthias Ottaaron hansBri Camp GomezPatrick HaugArihant VermaJake KorthelvendrimEleventywesruv 💻🐕Florian Geierstangerkonfuzedhenry from online ✷Emilio MartinezrickthehatAndy BellDave RupertThamara Gerig ☁🌈🌞Carles MuiñosMatt DeCamp
15 Comments
  1. Bri Camp Gomez

    @BrianaCamp

    "Are your JavaScripts making too much noise all the time?"

  2. Dave Rupert

    @davatron5000

    Do you use eleventy-img in your build pipeline? If so, what’s the cost? Or do you use it as a “sidecar” kind of tool to process images while you’re building you static site?

  3. James Doc

    @jamesdoc

    That is a nice touch

  4. Zach Leatherman

    @zachleat

    Thanks!

  5. Bruce Lawson

    @brucel

    i can deal with that code, and it looks top, thanks!

  6. Tristan Gibbs

    @gibbs_digital

    Thanks for writing this. I’ve been wanting to learn more about Eleventy Img 🙂

  7. Zach Leatherman

    @zachleat

    no plans for a CLI for now but maybe if a bunch of people ask 😅

  8. Zach Leatherman

    @zachleat

    DoN’t bUTTer ME Up unLeSS My HEAD iS stUck In THE STaiRcaSe bAnistEr brUCE

  9. Bruce Lawson

    @brucel

    "I was at the end of my tether, man.."

  10. Zach Leatherman

    @zachleat

    DangerOuSlY ClOSE tO A Sea ShanTY, heRe

  11. Simon Pieters

    @zcorpan

    That looks pretty slick. Two reactions: * `sizes` is required when using "w" * Maybe it could have a generateCSS that serializes `image-set()`

  12. Carol 🌻

    @CarolSaysThings

    Hey! This is great timing, I'm setting up eleventy-img on my site this week 🙌🏼 I do have a question, if that's ok, and feel free to point me to a more appropriate channel: If you set multiple widths for the images, generateHtml sets the width of the img tag to the sma… Truncated

  13. Zach Leatherman

    @zachleat

    wow this is going to be in my head all day—thanks max

  14. Bruce Lawson

    @brucel

    We're going to sign it to you every day until there's a CLI, and a nice GUI interface for Mac and Win

  15. JamstackTORONTO aka Jamstack U.N. 🇺🇳

    @JAMstackTORONTO

    you're always welcome Zach (#doubleEntendre). Apologies for the late deploy, but happy we finally got them out.

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)