phosphor-icons/core

[Feature request] Generate SVG sprites as a build step

Opened this issue · 2 comments

(Not sure if this is the right place for feature requests. Feel free to move it somewhere more appropriate)

Motivation

Performance (load times, rendering, memory)

This is a visualization of our current production app bundle (no worries! it's split into multiple chunks in case anyone gets dizzy). There's quite a bit going on. The biggest chunk of node_modules/ is @phosphor-icons which takes up ~300kb of JS, notably (the orange slice). This stems from us using @phosphor-icons/react.
Bildschirmfoto 2023-07-24 um 18 29 48

Instead it would be beneficial if all of this could move over here, into the assets/ part of the app, as a static *.svg asset that can be cached by browsers and does not clutter JS bundles more than necessary.
Bildschirmfoto 2023-07-24 um 18 30 11

As Jason Miller points out correctly

Please don't import SVGs as JSX. It's the most expensive form of sprite sheet: costs a minimum of 3x more than other techniques, and hurts both runtime (rendering) performance and memory usage.

Developer experience

Technically, this is possible today by downloading and using the individual *.svg files instead of relying on @phosphor-icons/react. This comes with a significant decline in developer experience though, some of which is described nicely in The "best" way to manage icons in React.js

  1. SVGs rendered as <img /> cannot be styled using CSS
  2. Without preloading as a <link rel="preload" />, SVGs rendered as <img /> introduce request waterfalls for the initial load. Adding an appropriate preload tag for each icon gets tiring
  3. All of @phosphor-icons/react's benefits such as easily importing any of the icons or changing the icon's weight via nothing more than a prop change becomes way more tedious

The blog's proposed solution is to render SVGs as a sprite

<!-- sprite.svg -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <symbol viewBox="0 0 24 24" id="icon-1">
      <!-- svg content here -->
    </symbol>
    <symbol viewBox="0 0 24 24" id="icon-2">
       <!-- svg content here -->
    </symbol>
  </defs>
</svg>

and reference them in a component (taking React as an example)

function Icon({ id, ...props }) {
  return (
    <svg {...props}>
      <use href={`/sprite.svg#${id}`} />
    </svg>
  );
}

This solves both the performance problems as well as 1. (can be styled with CSS) and 2. (only need to preload one file) but still requires manual work to update the SVG sprite by hand each time a new icon is added / the weight is changed / phosphor receives an update where icons are improved/changed.
This can be improved partially by using remix-cli which provides a way to create an SVG sprite by pointing it at a folder containing individual *.svg files. But most of the above still holds true.

Proposed solution

Putting together all of the above, it would be ideal to have a solution that

  • creates an SVG sprite containing all the actually used phosphor-icons
  • is as convenient to use as @phosphor-icons/react with (partial?) support of all its features

For this to work, I propose new packages @phosphor-icons/react-sprite and @phosphor-icons/vue-sprite that both offer a <PhosphorIcon /> much like the <Icon /> component above, offering an interface including all the IconProps & { name: "address-book" | "air-traffic-control" | ... }.

On top of that, there'll need to be a build process much like the one of TailwindCSS:
Read the contents of *.[jsx, tsx, vue| files, find all <PhosphorIcon />s and evaluate their props. Based on that, generate a sprite.svg that can be imported in one place.
Unfortunately this comes with a rather major caveat I believe: Just as the tailwind build process, this requires to only put static strings as props since otherwise this cannot be statically analyzed without going through the immense pain of somehow evaluating dynamic code. One alternative I can think of is to offer named components <AddressBookThin />, <AddressBookLight /> and the likes that under the hood all resolve to the same <PhosphorIcon /> for the sole purpose of being able to statically analyze the code for the build process. This still feels kinda meh but all things considered might still be worth it considering the upsides. There may be other ideas I haven't thought of of course.

I understand that this is a passion project built in your guys' free time and something like this isn't done quickly. Just wanted to share some ideas! :)

It's certainly a cool idea, for those needing to eke out better performance in this regard (though I doubt most users do, as a handful of icons, tree-shaken, is usually a few tens of kBs of JS -- you seem to be using quite a few!). Definitely something I considered when laying the groundwork for 1.0.

My main misgiving is the high maintenance cost, and further fragmentation of what is already a pretty large number of libraries and ports I maintain. Build tooling is complicated -- you have highlighted just a few of the many wrinkles you'd encounter trying to make a Tailwind-like solution that "just works" for this use-case. I unfortunately don't have the time or bandwidth to maintain such a thing (things) right now. It seems like a perfect opportunity for an enterprising contributor, though!

Orthogonal to this, I AM working on a font-stripping tool to generate minimal font+css bundles that can be statically built and included as public, cacheable assets: https://pack.phosphoricons.com. The tool is still in beta, and it unfortunately does not do any automagic at build time (you have to manually pick the icons you want), but still provides the exact ergonomics of using @phosphor-icons/web at dev time.

A (probably stupid) alternative just came to me:

  1. Clone this repo as a git submodule into your /public dir, or equivalent (host all of Phosphor assets on your domain)
  2. Create a PhosphorIcon react wrapper component that renders an <object type="image/svg+xml" data="<COMPUTED_RESOURCE_URL>" /> with some additional attributes to apply styles
  3. Use it like this:
<PhosphorIcon name="cube" weight="fill" size={72} color="red" />

It's dumb. It might be really bad to create a new Document for each icon. But it works:

https://stackblitz.com/edit/stackblitz-starters-ydxdwf?file=src%2FApp.tsx,src%2FPhosphor.tsx