Few Things I Learned while Developing an Icon Library
We use icons everywhere in web applications and pages. An icon is a graphical representation of meaning. Icons can be used to express actions, state, and even to categorize data. The Ant Design specification has mature design guidance for icons. And as its implementation for Angular, ng-zorro-antd provides an icon component and hundreds of icons for developers.
In the past, these icons are encapsulated in a font file. However, from v1.7.0 ng-zorro-antd started to use SVG in its icon library @ant-design/icons-angular.
SVG icons are good in many cases compared to font-based icons such as Font Awesome (but it supports SVG icons nowadays, too). It looks sharper on low-resolution devices. It could have multiple colors (ng-zorro-antd does support that). It could be bundled into your JavaScript files so you won’t have to fire another HTTP request. But there’s a problem that is a pain in the ass:
SVG is too big.
A font file is binary so it could be small in size. However, the SVG file is just a plain text file with a unique suffix name (.svg). Though you could gzip it (and web servers like nginx would do that for you), it’s still big and takes a good bite on your bundle size budget. So the challenge for us was,
How can we ship these icons to browsers at the lowest cost without breaking anything?
This article is all about how we managed to do that and what I’ve learned in this journey. If you are building an icon library or just getting interested, keep reading. ;)
I am from ng-zorro-antd core team. ng-zorro-antd is an enterprise-level UI component library for Angular and follows Ant Design specification. If you haven’t heard of it, you definitely should give it a try.
Two Approaches
There are two simple approaches.
The first is that we package all icons into the component library — just like the old days. But this issue of antd (Ant Design for React) stopped us from taking this approach — users do not want more 500KB of non-tree-shakable code in their bundles. (React community has come up with lots of solutions like this one to shrink the bundle size. But honestly, this is even more troublesome than the second approach that I am going to introduce below.)
The second is that we leave developers to import icons that they are going to use. By doing that, we won’t package anything unnecessary. This approach is neat and “how it should be”. However, we cannot go this way either because our users were used to render icons without importing them beforehand (thanks to the tiny, tiny font file). Adopting this approach would force users to write lots of import XXXIcon
to fix their projects. BREAKING CHANGE ALERT! (BTW, this article is a good tutorial if you want to create an icon library just for yourself.)
The problem seems to get more complicated. It’s a paradox that:
- On the first hand, we cannot afford to ship all icons because they are too large and users hate it when we do that.
- On the other hand, we must ship all icons to avoid breaking changes since we don’t know what users will eventually use.
So, are we running into a dead end? (Apparently not, because if so, you were not reading this right now! 😄)
Have you heard of Idle while Urgent or Lazy Initialization? We can instantiate a class just before we are going to use it, and we can also load an icon just before we are going to render one!
That’s why we adopted an approach that we called “dynamic loading”.
The Third Approach
You can get the source code of @ant-design/icons-angular here.
Dynamic loading means: when we want to render an icon that hasn’t been loaded, we load it from a remote server, cache it, and then render it.
Let’s explain this idea by diving into the source code. Don’t worry. I won’t cover every detail in this project, just the main process.
When we are going to render an icon, say an outlined “heart”, we first lookup the cache to see whether this icon has cached. If not, we will load it by calling _loadIconDynamically
.
// If `icon` is a `IconDefinition` of successfully fetch, wrap it in an `Observable`.
// Otherwise try to fetch it from remote.
const $iconDefinition = definitionOrNull
? rxof(definitionOrNull)
: this._loadIconDynamically(icon as string);
The request simply asks the server for the icon (SVG string), assemble an icon object, and cache it.
const source = !this._enableJsonpLoading
? this._http
.get(safeUrl, { responseType: 'text' })
.pipe(map(literal => ({ ...icon, icon: literal }))) // assemble an icon object, type as IconDefition
: this._loadIconDynamicallyWithJsonp(icon, safeUrl); // jsonp-like loading inProgress = source.pipe(
tap(definition => this.addIcon(definition)), // cache it
finalize(() => this._inProgressFetches.delete(type)), // delete the request object
catchError(() => rxof(null)),
share() // share with other subscribers
);
Now we get an icon object and area able to continue the rendering. Hooray! 🎊🎊🎊
But that’s just not enough. There are some details worth noticing.
If we have several same icons to render at the same time, it would be costly if we fire HTTP requests for each of them. So these icons should share the same request (named inProgess
). There’s a share operator that will share the icon object to all subscribers. And we remove the request after the request is over.
How could we know where to load icons? Thanks to Angular/CLI, we provide a schematic that could help users add icon assets to their bundle in a convenient way. When a developer is installing ng-zorro-antd with this command ng add ng-zorro-antd
, it would ask if s/he would like to add icons assets and modify angular.json if the developer wants so.
What if users want to load icons from a CDN? We have to make URLs of requests configurable. So we provide a method named changeAssetsSource
for users to set the URL prefix.
What if the CDN doesn’t support cross-domain XML requests? We provide a jsonp-like mechanism for this and developers could enable it by calling useJsonpLoading
.
And so on.
So you can see that even the core idea is simple, you must take lots of scenarios into consideration, though some of those you may never run into!
There was still lots of work to do:
- The second approach is great, and we should support it as well. So we endorsed static loading.
- We needed some scripts to generate icon resources.
- Our old API was no API (literally). Users just needed to write an
i
tag in this way<i class="anticon anticon-clock">
. So we needed to support the old API as well (we used MutationObserver). - We needed to implement features such as spinning, custom icon, namespaces and iconfont.
- Docs (very important).
Finally, I wrote @ant-design/icons-angular and rewrote the Icon component of ng-zorro-antd.
@ant-design/icons-angular, as an underlying dependency, provides icon resources and fundamental features such as static loading, dynamic loading, jsonp-like loading, and namespace.
The Icon component of ng-zorro-antd is responsible for the dirty work (adapting old API), and providing add-on features such as spinning.
Conclusion
In October 2018, we release v1.7.0 with the new icon component. And I wrote a detailed upgrade guide to explain why we replaced old font-based icons with SVG icons, and what should developers do if they want to upgrade to the new version. Thankfully, our users felt comfortable with this new version and willingly moved along with us. (You guys are awesome. Thanks!)
As a summary, what have we achieved?
- We use SVG to render icons and ship as less code as possible.
- We helped our users to upgrade painlessly, avoided breaking things.
Nice! It looks like we solved the problem in an elegant way. What about performance? You may ask.
Well, the doc webpage of the Icon component which has hundreds of icons is strong proof that the performance is merely harmed. In fact, you may not notice that the icons are loaded dynamically if you only use dozens of them. Your website is a PWA (like our official website)? End of the discussion!
Here is what I learned as an open source library author by working on this project:
- Think about how changes you make will influence your users, and think twice. Try not to make breaking changes. Adapt the old API and give users have time to migrate to the new. Stick to the semantic versioning system.
- Don’t make things hard to revert and definitely not make decisions for your users. Instead, provide choices. It’s not a wise decision for the antd team to import all icons and not to provide a more flexible way (such as dynamic loading). And we learned from their mistakes.
- Work hard before you reach a conclusion. Keep asking yourself “is there a better way to do this?”
That’s all. Thank you for reading.
Hi, I am Wendell from ng-zorro-antd core team. Author of the Icon component of ng-zorro-antd and @ant-design/icons-angular. Follow me 🐦 if you want to read more about ng-zorro-antd, Angular, React and other things happening in the front end world. And you can also follow the official account of NG_ZORRO 🐦 to get tuned!