Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add <FaStaticSprite> placeholder to transform icons #138

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

jrjohnson
Copy link
Collaborator

@jrjohnson jrjohnson commented Mar 1, 2020

This adds a template placeholder <FaStaticSprite> which will be transformed at build time into a plain DOM element with a link to the sprite for the icon.

Eg:

<FaStaticSprite @prefix="fas" @icon="coffee" />

becomes

<svg 
class="svg-inline--fa fa-coffee fa-w-20"
role="img"
focusable="false"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
  <use xlink:href="assets/fa-sprites/solid.svg#coffee"></use>
</svg>

Sprite files for included icon sets are added to assets/fa-sprites automatically.

This realizes many of the benefits of the enableExperimentalBuildTimeTransform without some of the drawbacks indicated in #117.

I did some limited performance tests, rendering 3K coffee icons on the screen using this transform takes about 600ms whereas using the component it's around 3 seconds. So this is significantly faster at very large scale.

This will move sprite files from any installed fontawesome builds into the public directory so they're available for use.
This placeholder value will now be transformed into a static html svg
element that references the correct sprite.
Each icon has default width and height information applied through
classes. Adding them to the sprite ensures that sprite icons and
component icons are visually interchangeable.
@jelhan
Copy link
Contributor

jelhan commented Mar 3, 2020

This is going in the correct direction in my opinion. I like that it's explicit. Both for developer and compiler it's clear that this most be transformed to a static DOM element at build time. This seems to be also inline with the overall direction the ember ecosystem is going. E.g. the macro system proposed by Embroider RFC also adds a {{macroCondition}} for the same explicitly.

Some open questions from my side:

  • Is there an impact on build-time? If it slows down initial build and rebuild an option to disable static transform for development environment might be needed.
  • Is there an impact on bundle size? The increased bundle size was listed as a main drawback of the current solution.
  • Could we throw an build time error if an icon is used for in a <FaStaticSprite> that is not included? Or should it be included magically? I prefer the first option as it's more explicit.
  • Are their still drawbacks of using <FaStaticIcon @prefix="fas" @icon="coffee" /> instead of <FaIcon @prefix="fas" @icon="coffee" />? If not: Should we add a build- or run-time warning to push developers in the right direction?
  • Did you considered using <FaIconStatic> instead of <FaStaticIcon>? It might improve readability of the code if both variants of the component starts with the same string.

@st-h
Copy link
Contributor

st-h commented Mar 3, 2020

  • Could we throw an build time error if an icon is used for in a <FaStaticSprite> that is not included? Or should it be included magically? I prefer the first option as it's more explicit.

Please don't. This currently is the most annoying thing about this addon: Having to manually list all the icons you use. And if someone isn't careful he/she forgets to remove the icon from the list, which just got deleted in all other places. If people really want to write things twice and manually parse the app to figure out what icons need to be listed, please make it configurable, so everybody else can let the machines do that kind of work.

@@ -48,8 +48,10 @@
]
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.12.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentionally added?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's where the sprites seem to live, are they somewhere else? I figured a zero install experience for free icons was better than documenting a separate install step.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, they live there and that's the correct place to get them from. But this would force people who are just using Pro to download the extra package. I also worry about someone who is trying to use an older version for one reason or another (we have those folks who aren't ready to upgrade) This is going to add a competing version of the package that might have to contend with hoisting.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I'll replace with installation instructions.

@robmadole
Copy link
Member

I have some thoughts:

  1. As far as applying the svg-inline--fa fa-coffee fa-w-20 classes to the sprites I think it's a good idea. To the best of my knowledge those behave just like a regular SVG with regard to styling.
  2. How do the sprites load? Does the entire sprites/*.svg file get loaded somehow as a static file? I wonder since the sprite files are just simple SVG/XML files if it would be worth transforming those and subsetting those down to just the icons being used. (After all, we know which icons are being used)
  3. This sprite technique is not supported in Internet Explorer, is that OK? Does there need to be a warning? Our (Font Awesome's) position is: it's fine if not everything is supported in IE.
  4. Also @jrjohnson wanted to make sure you're aware this exists (no action on it, just thought that information needed to be in your head at this time)

This is a great idea. We've had more and more interest in rendering the icons at build time (Next.js, Nuxt.js, etc.) so this is a good direction in my opinion.

@jrjohnson
Copy link
Collaborator Author

Thanks everyone - great questions and information.

@jelhan

Is there an impact on build-time? If it slows down initial build and rebuild an option to disable static transform for development environment might be needed.

I tested with 5K icons in the dummy app and it added less than one second to the total build time. With no icons included in templates it doesn't change build time in a measurable way.

Is there an impact on bundle size? The increased bundle size was listed as a main drawback of the current solution.

Yes, currently the bundle size grows if there are more icons. It's better than the current solutions because the link to the sprite is not as verbose as the SVG itself. I think this is likely a bug in my implementation though, so I'm hoping for some help in discord to solve it.

Could we throw an build time error if an icon is used for in a that is not included? Or should it be included magically? I prefer the first option as it's more explicit.

Right now all icon sprite files are being included magically, but they are placed into /public and not loaded until they are needed. I do think some kind of warning if you're using an icon that doesn't exist is probably appropriate, I'll add it. @st-h I think this would address your comments as well because you don't have to list icons explicitly they just exist in the sprite file. The only thing an error would catch here is a typo mistake where you've referenced an icon that the library does not know about.

Are their still drawbacks of using <FaStaticIcon @prefix="fas" @icon="coffee" /> instead of <FaIcon @prefix="fas" @icon="coffee" />? If not: Should we add a build- or run-time warning to push developers in the right direction?

I'd like to test this in the wild before recommending a change, but I do think that for purely static invocations this may be the better path for most usage. The API for things like spin and transform might be more difficult though so it might not be the right choice every time.

Did you considered using instead of ? It might improve readability of the code if both variants of the component starts with the same string.

This is one thing I really waffled on. I'm ok with either.

@robmadole

How do the sprites load? Does the entire sprites/*.svg file get loaded somehow as a static file? I wonder since the sprite files are just simple SVG/XML files if it would be worth transforming those and subsetting those down to just the icons being used. (After all, we know which icons are being used)

Yes, the entire sprite loads the first time it is used. I'm not thrilled with this, but I haven't yet figured out how to detect icon usage in time to modify the build because of the way the ember build works. I think if we get that ability this would be a great enhancement.

This sprite technique is not supported in Internet Explorer, is that OK? Does there need to be a warning? Our (Font Awesome's) position is: it's fine if not everything is supported in IE.

Thanks for the reminder, I meant to add this to the docs. I think a warning in the docs should be sufficient and allow application owners to decide how to proceed.

@st-h
Copy link
Contributor

st-h commented Mar 7, 2020

I'd like to test this in the wild before recommending a change, but I do think that for purely static invocations this may be the better path for most usage.

If you have anything to try I will be happy to give it a spin. I just tried to instrument our app via ember-perf-timeline and to my surprise it showed that rendering the FaIcons takes a lot more time than I expected. We use quite a lot of FaIcons (especially to make buttons easier to recognise), so every increase in performance will help.

@oliverlj
Copy link

Did some performance test :
image

351 ms with the sprite instead of 1.46s for 208 icons in each component

@oliverlj
Copy link

https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href => xlink:href will be deprecated in svg 2

const title = this.builders.text(mappedAttributes['@title'].chars);
children.push(this.builders.element('title', null, null, [title]));
}
const xlink = this.builders.attr('xlink:href', this.builders.text(`${spritePath}#${iconName}`));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without the leading / on ${spritePath} , it is not looking on root url /assets but /user/assets for example

@oliverlj
Copy link

css styles do not apply to svg => color become fill to changes icon color

@st-h
Copy link
Contributor

st-h commented Apr 8, 2020

If I use your branch in package.json:
"@fortawesome/ember-fontawesome": "jrjohnson/ember-fontawesome#static-icon-transform",

And add this icon:

<FaStaticSprite @icon="comment-alt-lines" />

I see this error:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property '0' of null
    at normalizeElementOptions (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:225:1)
    at Object.buildElement [as element] (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:285:1)
    at FaStaticSpriteTransformPlugin.transformElementNode (myApp/node_modules/@fortawesome/ember-fontawesome/lib/fa-static-sprite-transform.js:110:28)
    at ElementNode (myApp/node_modules/@fortawesome/ember-fontawesome/lib/fa-static-sprite-transform.js:53:33)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1356:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1427:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1427:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1427:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at visitArray (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1447:1)
    at visitKey (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1424:1)
    at visitNode (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1378:1)
    at traverse (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:1488:1)
    at preprocess (myApp/node_modules/ember-source/dist/@glimmer/syntax.js:2525:1)
    at precompile (myApp/node_modules/ember-source/dist/@glimmer/compiler.js:2163:1)
    at Object.precompile (myApp/node_modules/ember-source/dist/ember-template-compiler/lib/system/precompile.js:28:1)
    at Object.template (myApp/node_modules/ember-cli-htmlbars/lib/utils.js:178:38)
    at TemplateCompiler.processString (myApp/node_modules/ember-cli-htmlbars/lib/template-compiler-plugin.js:72:15)
    at Promise.then.output (myApp/node_modules/broccoli-persistent-filter/lib/strategies/persistent.js:56:23)
    at initializePromise (myApp/node_modules/rsvp/dist/lib/rsvp/-internal.js:236:5)
    at new Promise (myApp/node_modules/rsvp/dist/lib/rsvp/promise.js:143:33)
    at myApp/node_modules/broccoli-persistent-filter/lib/strategies/persistent.js:55:18
    at tryCatch (myApp/node_modules/async-disk-cache/node_modules/rsvp/dist/lib/rsvp/-internal.js:198:12)
    at invokeCallback (myApp/node_modules/async-disk-cache/node_modules/rsvp/dist/lib/rsvp/-internal.js:211:13)
    at publish (myApp/node_modules/async-disk-cache/node_modules/rsvp/dist/lib/rsvp/-internal.js:181:7)
    at flush (myApp/node_modules/async-disk-cache/node_modules/rsvp/dist/lib/rsvp/asap.js:80:5)
    at processTicksAndRejections (internal/process/task_queues.js:79:11)

I have no idea why I am seeing this...
Ember version: 3.17.3

adding ?fastboot=false to the url does not help

@jrjohnson
Copy link
Collaborator Author

Thanks @st-h, I heard that the glimmer API changed substantially in 3.17. It's one of my hesitations about adding this type of build time transform since that API isn't under semver. I'll investigate.

@jrjohnson
Copy link
Collaborator Author

jrjohnson commented Apr 19, 2020

OK, I have some performance numbers for discussion. (thanks @st-h for digging up the right way to do this).
They're a bit artificial in that rendering 10K coffee icons to the page is not a super normal event, but I think they'll do for now. What I did was create:

  1. a component that used the sprite file instead of the SVG
  2. a template only component that used the sprite file

Then I created a bunch of routes to render 1K, and 10K icons by placing one on each line:

<FaStaticSprite @icon="coffee" />
<FaStaticSprite @icon="coffee" />
<FaStaticSprite @icon="coffee" />
...

of in a loop as:

{{#each (repeat 1000) as |empty|}}
  <FaStaticSprite @icon="coffee" />
{{/each}}
...

First of all a note on file size: Adding 10K build time transformed coffee icons results in a 3MB (18kb gzipped) of additional javascript over a component or loop invocation. This is because each line in the template gets transformed into a string that is included in the final output. While the over network size of this is fairly small at 18kb unzipping and parsing MBs of extra JS is going to have a significant performance penalty.

I then used the CLI version of lighthouse to collect two stats:

FCP => First Contentful Paint
TtI => Time to Interactive

$lighthouse http://localhost:4200/ROUTE --only-categories=performance

The Results::
<FaIcon
1K FCP: 4.8s
1K TtI: 4.8s
10K FCP: could not measure as it was too slow
10K TtI: could not measure as it was too slow

<FaIcon Loop
1K FCP: 4.6s
1K TtI: 4.7s
10K FCP: could not measure as it was too slow
10K TtI: could not measure as it was too slow

<FaStaticSprite (built time transform)
1K FCP: 3.8s
1K TtI: 3.8s
10K FCP: 10.7
10K TtI: 11.7s

<FaStaticSprite loop
1K FCP: 3.9s
1K TtI: 3.9s
10K FCP: 11.6
10K TtI: 12.7

<FaIconSpriteComponent (with JS class)
1K FCP: 4.4s
1K TtI: 4.5s
10K FCP: 12.5s
10K TtI: 13.3s

<FaIconSpriteComponent Loop
1K FCP: 4.5s
1K TtI: 4.6s
10K FCP: 12.5s
10K TtI: 13.5s

<FaIconSpriteTemplateOnlyComponent
1K FCP: 5.2s
1K TtI: 5.2s
10K FCP: 12.5s
10K TtI: 14.4s

<FaIconSpriteTemplateOnlyComponent Loop
1K FCP: 5.3s
1K TtI: 5.3s
10K FCP: 12.5
10K TtI: 13.3


The template only component is so slow because it's necessary to use a {{#let in order to parse out enough information from an abstract icon to do this work or else pass in a LOT of options making for a pretty rough API. I'm going to to ahead and rule that out for now. Leaving aside API issues and just creating a template only component with static HTML it wasn't any faster than then JS class backed one. Score one for Glimmer, it's very very fast.

I tested the <FaIconSpriteComponent JS class component with both a glimmer and classic component and there wasn't a big difference. So I'm comfortable saying that the speedup between the existing FaIcon component and the sprite based on is due to the SVG sprites. This isn't surprising as they are suposed to be much more performant in the browser.


Given the difficulty in maintaining a build time transform and the additional complexity this will add as ember moves towards embroiderer for a build. Plus the fact that minor versions of ember may included breaking changes to the glimmer API I'm tending towards closing this PR and instead adding a regular <FaIconSprite component that will have nearly all of the performance benefits of this build time transform and much less complexity. I'll design the API for that new component to be compatible with a build time transform so this option can be re-examined once embroider become the default build and then we can allow apps to opt into the 10% rendering boost in trade for a larger JS file.

Am I reading these numbers correctly? Are there performance or DX issues I'm not thinking of?

@lupestro
Copy link

An option that brings the needed SVG glyphs into the page itself and references them from there, rather than externally, will work with IE11. I know this because we had to do this with the SVG icon symbol library (sprite) for our app.

@boesi boesi mentioned this pull request Aug 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants