Deno Fresh SVG Sprites: Optimized Icons

Rodney Lab - Aug 1 '23 - - Dev Community

📝 SVG Icon Optimization

We will have a look at SVG Icon optimization, in this Deno Fresh SVG sprites post. Without care, SVG icons can take up half of the bytes of HTML code shipped for a particular page. This is especially true working in React and embedding the SVG inline, in React components.

In this post, we will see how you can create an SVG sprite sheet for all icons on a page, or a site. Loading that sheet as an independent, resource from an HTML use tag is a more performant alternative than adding the SVG content inline. We shall see, this approach also allows you easily to style the icons.

We see further optimizations, making the sprite sheet a static asset with Deno Fresh’s cache-busting feature. Finally, you get some example code for using the SVGO plugin in Deno to minify the sprite sheet. Hopefully, this is what you were looking for, so let’s get going. The code and approach is based on an excellent blog post by Ben Adam, which goes into the details of how it works together with limitations. We won’t revisit those aspects here.

🧱 What are we Building?

Deno Fresh SVG Sprites: Screen capture shows a website with a Deno logo and a lemon icon.  The Deno icon depicts a black dinosaur head in profile with a rainy backdrop, in a cartoon style. The lemon is yellow, with two green leaves and a short stalk.

I put together a basic demo for the post, though you should be able just to copy and paste code snippets into a new or existing project, which you are already working on. There is a link to the full demo code further down.

Using these techniques, I reduced the size of shipped HTML by more than 25% (after compression) for a site I was working on.

🔎 Finding Icons

If you are not new here, you probably already know that Icônes by Anthony Fu is my “go to” source of SVG icons. It is a huge library with dozens of icon collections, and you are unlikely to be unable to find any icon you are looking for there.

Although Icônes provides downloadable React components for each icon, here we need the raw SVG. To get going, go to the Icônes all-collection search page to find and select an icon.

Deno Fresh SVG Sprites: Screen capture shows the Icôones website with the Simple Icons Deno icon selected.  The SVG snippet button is highlighted.

When you select your chosen icon, a pop-up will appear at the bottom of the window with the various format options. From Snippets, select SVG. This will copy the SVG code to your clipboard.

Example SVG Icon Code

Here is the SVG code for the Simple Icons Deno icon (used in the example image above):

<svg
  xmlns="http://www.w3.org/2000/svg"
  width="32"
  height="32"
  viewBox="0 0 24 24"
>
  <path
    fill="currentColor"
    d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12S5.373 0 12 0Zm-.469 6.793c-3.49 0-6.204 2.196-6.204 4.928c0 2.58 2.498 4.228 6.37 4.145l.118-.003l.425-.012l-.109.279l.013.029c.031.072.06.145.084.22l.01.028l.015.045l.021.065l.014.045l.014.047l.015.049l.021.075l.022.079l.015.054l.023.084l.022.088l.023.091l.023.095l.015.065l.024.1l.023.103l.032.143l.017.074l.024.114l.024.117l.025.12l.035.174l.029.142l.037.195l.02.1l.028.155l.03.158l.039.217l.04.225l.04.231l.041.24l.042.246l.042.254l.042.26l.032.201l.055.344l.022.14l.055.36l.045.295l.034.227l.046.308l.023.156a10.758 10.758 0 0 0 6.529-3.412l.05-.055l-.238-.891l-.633-2.37l-.395-1.47l-.348-1.296l-.213-.787l-.136-.498l-.081-.297l-.073-.264l-.032-.11l-.018-.064l-.01-.034l-.008-.026a6.042 6.042 0 0 0-2.038-2.97c-1.134-.887-2.573-1.351-4.252-1.351ZM8.467 19.3a.586.586 0 0 0-.714.4l-.004.013l-.527 1.953c.328.163.665.309 1.008.437l.08.03l.57-2.114l.004-.015a.586.586 0 0 0-.417-.704Zm3.264-1.43a.586.586 0 0 0-.715.4l-.004.014l-.796 2.953l-.004.014a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.003-.014a.585.585 0 0 0 .013-.067l.002-.022l-.019-.096l-.027-.138l-.018-.086a.584.584 0 0 0-.367-.295Zm-5.553-3.04a.59.59 0 0 0-.037.09l-.005.02l-.797 2.953l-.004.014a.586.586 0 0 0 1.131.306l.004-.014l.723-2.678a5.295 5.295 0 0 1-1.015-.692Zm-1.9-3.397a.586.586 0 0 0-.715.4l-.004.013l-.797 2.953l-.003.015a.586.586 0 0 0 1.13.305l.005-.014l.797-2.953l.003-.015a.586.586 0 0 0-.416-.704Zm17.868-.67a.586.586 0 0 0-.715.399l-.004.014l-.797 2.953l-.003.014a.586.586 0 0 0 1.13.305l.005-.014l.797-2.953l.003-.014a.586.586 0 0 0-.416-.704ZM2.542 6.82a10.707 10.707 0 0 0-1.251 3.926a.586.586 0 0 0 1.002-.22l.004-.014l.797-2.953l.003-.014a.586.586 0 0 0-.555-.725Zm17.585.02a.586.586 0 0 0-.714.4l-.004.014l-.797 2.953l-.004.014a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.004-.014a.586.586 0 0 0-.417-.704Zm-7.846 1.926a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5Zm-6.27-4.733a.586.586 0 0 0-.715.398l-.004.015l-.797 2.953l-.004.014a.586.586 0 0 0 1.132.305l.003-.014l.797-2.953l.004-.014a.586.586 0 0 0-.417-.704Zm10.238.558a.586.586 0 0 0-.714.399l-.004.014l-.536 1.984c.347.171.678.373.99.603l.051.038l.626-2.32l.004-.014a.586.586 0 0 0-.417-.704Zm-5.211-3.33a10.76 10.76 0 0 0-1.115.158l-.078.015l-.742 2.753l-.004.015a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.004-.015a.583.583 0 0 0 .003-.264Zm7.332 2.04l-.156.58l-.004.015a.586.586 0 0 0 1.131.305l.004-.014l.017-.063a10.838 10.838 0 0 0-.923-.772l-.069-.051Zm-4.636-1.944l-.283 1.048l-.003.014a.586.586 0 0 0 1.13.305l.005-.014l.297-1.102c-.35-.097-.705-.176-1.063-.237l-.083-.014Z"
  />
</svg>
Enter fullscreen mode Exit fullscreen mode

You can see the full SVG for just one, simple icon is already a fair few bytes! In the next section, we start building the component which pulls in the path from a sprite sheet, rather than inlining it.

🧩 Creating an SVG Component with use Element

Here is the code for a Preact component, which will produce the SVG icon above. The file path in the demo code is components/Icons/Deno.tsx:

import { asset } from "$fresh/runtime.ts";
import { FunctionComponent } from "preact";

interface DenoIconProps {
  colour?: string;
  height?: number;
  width: number;
}

export const DenoIcon: FunctionComponent<DenoIconProps> = function DenoIcon({
  colour = "inherit",
  width,
  height = width,
}) {
  // SVG path by Simple Icons (https://simpleicons.org/) via Icônes (https://icones.js.org/collection/all)

  return (
    <svg
      width={`${width}px`}
      height={`${height}px`}
      style={{ color: colour }}
    >
      <use href={`${asset("/sprite.svg")}#deno`} />
    </svg>
  );
};
Enter fullscreen mode Exit fullscreen mode

Following Ben’s method, the Preact component is an HTML use tag sandwiched inside an SVG tag. We can include styles on the SVG tag, and the path will go into the sprite sheet. The final sprite sheet will be a static asset in the static directory of the project. deno id in the href path is our own identifier, used to pick the right icon from the sprite sheet.

The Deno Fresh cache-busting comes from wrapping our path in the asset function (imported from $fresh/runtime in line 1). Deno will add a hash to the end of the path in the output site. This lets us set a long cache time, since the path will change whenever the sprite sheet changes, and the new sheet will be fetched, rather than recycling any cached version.

🥤 Adding the SVG Icon to the Sprite Sheet

The base sprite template will look like this (file path is assets/sprite.svg in the demo code):

<svg
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
>
  <defs>
  </defs>
</svg>
Enter fullscreen mode Exit fullscreen mode

We will add each icon as a symbol element, with id inside the defs element. In the next section, we optimize this file and output the optimized version to static/sprite.svg. Here is the completed version with a couple of icons:

<svg
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
>
  <defs>
    <symbol viewBox="0 0 24 24" id="deno">
      <path
        fill="currentColor"
        d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12S5.373 0 12 0Zm-.469 6.793c-3.49 0-6.204 2.196-6.204 4.928c0 2.58 2.498 4.228 6.37 4.145l.118-.003l.425-.012l-.109.279l.013.029c.031.072.06.145.084.22l.01.028l.015.045l.021.065l.014.045l.014.047l.015.049l.021.075l.022.079l.015.054l.023.084l.022.088l.023.091l.023.095l.015.065l.024.1l.023.103l.032.143l.017.074l.024.114l.024.117l.025.12l.035.174l.029.142l.037.195l.02.1l.028.155l.03.158l.039.217l.04.225l.04.231l.041.24l.042.246l.042.254l.042.26l.032.201l.055.344l.022.14l.055.36l.045.295l.034.227l.046.308l.023.156a10.758 10.758 0 0 0 6.529-3.412l.05-.055l-.238-.891l-.633-2.37l-.395-1.47l-.348-1.296l-.213-.787l-.136-.498l-.081-.297l-.073-.264l-.032-.11l-.018-.064l-.01-.034l-.008-.026a6.042 6.042 0 0 0-2.038-2.97c-1.134-.887-2.573-1.351-4.252-1.351ZM8.467 19.3a.586.586 0 0 0-.714.4l-.004.013l-.527 1.953c.328.163.665.309 1.008.437l.08.03l.57-2.114l.004-.015a.586.586 0 0 0-.417-.704Zm3.264-1.43a.586.586 0 0 0-.715.4l-.004.014l-.796 2.953l-.004.014a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.003-.014a.585.585 0 0 0 .013-.067l.002-.022l-.019-.096l-.027-.138l-.018-.086a.584.584 0 0 0-.367-.295Zm-5.553-3.04a.59.59 0 0 0-.037.09l-.005.02l-.797 2.953l-.004.014a.586.586 0 0 0 1.131.306l.004-.014l.723-2.678a5.295 5.295 0 0 1-1.015-.692Zm-1.9-3.397a.586.586 0 0 0-.715.4l-.004.013l-.797 2.953l-.003.015a.586.586 0 0 0 1.13.305l.005-.014l.797-2.953l.003-.015a.586.586 0 0 0-.416-.704Zm17.868-.67a.586.586 0 0 0-.715.399l-.004.014l-.797 2.953l-.003.014a.586.586 0 0 0 1.13.305l.005-.014l.797-2.953l.003-.014a.586.586 0 0 0-.416-.704ZM2.542 6.82a10.707 10.707 0 0 0-1.251 3.926a.586.586 0 0 0 1.002-.22l.004-.014l.797-2.953l.003-.014a.586.586 0 0 0-.555-.725Zm17.585.02a.586.586 0 0 0-.714.4l-.004.014l-.797 2.953l-.004.014a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.004-.014a.586.586 0 0 0-.417-.704Zm-7.846 1.926a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5Zm-6.27-4.733a.586.586 0 0 0-.715.398l-.004.015l-.797 2.953l-.004.014a.586.586 0 0 0 1.132.305l.003-.014l.797-2.953l.004-.014a.586.586 0 0 0-.417-.704Zm10.238.558a.586.586 0 0 0-.714.399l-.004.014l-.536 1.984c.347.171.678.373.99.603l.051.038l.626-2.32l.004-.014a.586.586 0 0 0-.417-.704Zm-5.211-3.33a10.76 10.76 0 0 0-1.115.158l-.078.015l-.742 2.753l-.004.015a.586.586 0 0 0 1.131.305l.004-.014l.797-2.953l.004-.015a.583.583 0 0 0 .003-.264Zm7.332 2.04l-.156.58l-.004.015a.586.586 0 0 0 1.131.305l.004-.014l.017-.063a10.838 10.838 0 0 0-.923-.772l-.069-.051Zm-4.636-1.944l-.283 1.048l-.003.014a.586.586 0 0 0 1.13.305l.005-.014l.297-1.102c-.35-.097-.705-.176-1.063-.237l-.083-.014Z"
      />
    </symbol>
    <symbol viewBox="0 0 512 512" id="lemon">
      <!-- TRUNCATED -->
    </symbol>
  </defs>
</svg>
Enter fullscreen mode Exit fullscreen mode

Notice:

  • in lines 6 and 14, we use the viewBox values from the source SVG;
  • we place the path elements inside the symbol tag; and
  • we must remember to add an id matching the id given in the corresponding component.

If your SVG has a defs tag, just include the icon’s defs tag contents above the symbol element for your icon.

🔥 Optimizing the Sprite

As a final step, you can add a Deno script to optimize the static, served sprite. First, update deno.json:

{
  "lock": false,
  "tasks": {
    "start": "deno run -A --watch=static/,routes/ dev.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update .",
    "minify": "deno run --allow-env=DENO_ENV --allow-read --allow-run --allow-write minify-svg-sprite.ts"
    },
  "imports": {
    "@/": "./",
    "$fresh/": "https://deno.land/x/fresh@1.2.0/",
    "preact": "https://esm.sh/preact@10.15.1",
    "preact/": "https://esm.sh/preact@10.15.1/",
    "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.1.0",
    "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
    "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
    "$std/": "https://deno.land/std@0.193.0/",
    "svgo": "https://esm.sh/svgo@3.0.2/"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then create the script minify-svg-sprite.ts in the project root directory:

import { optimize } from "svgo";

const svgString = await Deno.readTextFile("./assets/sprite.svg");
const { data: optimisedSVG } = optimize(svgString, {
  path: "./assets/sprite.svg",
});
await Deno.writeTextFile("./static/sprite.svg", optimisedSVG);

Deno.exit(0);
Enter fullscreen mode Exit fullscreen mode

You can now use the SVG icon components on your site pages:

import { DenoIcon } from "@/components/Icons/Deno.tsx";
import { LemonIcon } from "@/components/Icons/Lemon.tsx";

export default function Home() {
  return (
      <main>
        <DenoIcon colour="#fff" width={192} />
        <LemonIcon width={192} />
      </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

💯 Deno Fresh SVG Sprites: Checking Your Work

Run the minify script, and check it outputs a minified version of the sprite to static/sprite.svg. Remember to re-run this script whenever you add new icons to the sprite.

If you pull open dev tools and have a look at the Network tab, you should see the sprite is downloaded. The path should have a hash on the end of it (something like sprite.svg?__frsh_c=8d89...).

Screen capture show the Network tab in Firefox Developer Tools.  A GET request for the SVG sprite sheet is highlighted, show the sprite.svg url, with a hash appended as a query parameter.  The auxialiary pane, to the right shows an empty white rectangle and meta below states the SVG dimensions are 0 x 0.

Jumping onto the Inspector tab, use should find a use element, instead of the inline SVG code for the icon (though you can expand the enclosed #shadow-root to inspect the SVG included from the sprite).

Screen capture show the Inspector tab in Firefox Developer Tools. An SVG tag is highlighed in the main window.  In Inspector itself, within the svg tag is a use tag, rather than the inline SVG.  The href on the use tag is set to /sprite.svg, with a hash appended as a query parameter.

🙌🏽 Deno Fresh SVG Sprites: Wrapping Up

We saw how you can set up Deno Fresh SVG Sprites in your own project. In particular, we saw:

  • how you can use Icônes to find SVG icons;
  • how you make sure create an SVG sprite sheet for use with Preact components; and
  • a way to optimize SVG assets in Deno.

The complete code for this post. I do hope the post has either helped you with an existing project or provided some inspiration for getting started with setting up a new one.

Get in touch if you have some questions about this content or suggestions for improvements. You can add a comment below if you prefer. Also, reach out with ideas for fresh content on Deno or other topics!

🙏🏽 Deno Fresh SVG Sprites: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as Deno. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .