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

[icons] Enable tree-shaking for SVG icons #2356

Closed
wants to merge 39 commits into from

Conversation

reiv
Copy link
Contributor

@reiv reiv commented Apr 3, 2018

Fixes #2193

Changes proposed in this pull request:

This PR restructures the icons package to enable tree shaking for SVG icons. This should reduce the build size in production by quite a bit (around 350-400 KiB). Because the current icons architecture is incompatible with tree shaking, this optimization needs to be enabled with a compile flag, tentatively named BLUEPRINT_ICONS_TREE_SHAKING. Enabling this flag means that the icon prop of Icon components can no longer be specified as a string, and prevents the entirety of the icons package from being bundled by default. Instead, it falls to the consumer to manually import whichever icons are needed in that project. SVG icons are exported at the root level of the icons package, using aliases that are camelCased PascalCased versions of the icon names suffixed with "Icon". For example:

import { AlignLeftIcon, FunctionIcon } from "@blueprintjs/icons";

const icon = <FunctionIcon ... />;

Some Webpack build sizes:

Without tree shaking

  Asset     Size  Chunks                    Chunk Names
main.js  536 KiB       0  [emitted]  [big]  main

With tree shaking

  Asset     Size  Chunks             Chunk Names
main.js  138 KiB       0  [emitted]  main

The tree shaking flag can be set using webpack's DefinePlugin as follows:

new webpack.DefinePlugin({
    "global.BLUEPRINT_ICONS_TREE_SHAKING": true,
})

This will break everything that uses string-form icon props (nothing has been ported over yet).

@reiv
Copy link
Contributor Author

reiv commented Apr 3, 2018

Bail if icon name is invalid

Preview: documentation | landing | table

@llorca llorca requested a review from giladgray April 3, 2018 23:48
Copy link
Contributor

@giladgray giladgray left a comment

Choose a reason for hiding this comment

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

🎆 @reiv this is some great stuff! i'm mildly uncomfortable with the SVGIcon type and the functionIcon naming scheme, but i see why you went that route. i wonder what it would take to have FunctionIcon be an actual JSX element?

@@ -24,6 +24,7 @@
"removeComments": false,
"sourceMap": false,
"stripInternal": true,
"target": "es5"
"target": "es5",
"typeRoots": ["../node_modules/@types"]
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this necessary now? we've never needed it in the past.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was just something I had to add on my end because of a bug with at-loader on Windows. In particular, it was causing Typescript to complain about being unable to find require(). There's some discussion of it here: PatrickJS/PatrickJS-starter#1371
It shouldn't affect anything else, but happy to split this off into a separate PR to reduce noise in the diff.

Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm core declares the require() type. we should probably just do the same in icons rather than changing the tsconfig.

Copy link
Contributor Author

@reiv reiv Apr 5, 2018

Choose a reason for hiding this comment

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

However, this also fixes a slew of errors that occur as a result of that at-loader + Windows interaction. For example, it doesn't pick up the typings for Mocha, so compiling unit tests gives me a wall of can't find "it/describe/etc" errors. I'll definitely submit this as a separate PR then.

ICONS_METADATA.map(async icon => {
const filepath = path.resolve(__dirname, `../../resources/icons/${size}px/${icon.iconName}.svg`);
const svg = fs.readFileSync(filepath, "utf-8");
const pathStrings = await svgo
.optimize(svg, { path: filepath })
.then(({ data }) => data.match(/ d="[^"]+"/g) || [])
.then(paths => paths.map(s => s.slice(3)));
return ` "${icon.iconName}": [${pathStrings.join(",\n")}],`;
paths[icon.iconName] = `[${pathStrings.join(",\n")}]`;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is now a reduce, not a map.

ICONS_METADATA.reduce(async (paths, icon) => { ... }, {})

*/
function buildPathsObject(paths) {
return Object.keys(paths).map(iconName => {
return ` "${iconName}": ${paths[iconName]},`
Copy link
Contributor

Choose a reason for hiding this comment

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

inline! iconName => "template-string"

@reiv
Copy link
Contributor Author

reiv commented Apr 5, 2018

@giladgray If ${name}Icon is too verbose, how about pt${name}?

i wonder what it would take to have FunctionIcon be an actual JSX element?

Open to exploring this option as well (I was thinking about it too). All it would take is exporting the icons as SFCs or something; basically a function that returns an <Icon> element with the icon pre-filled in. I am concerned about the overhead that would add, though. Also, we'd get a weird circular dependency between icons and core. Is there a reason <Icon> should be defined in core and not icons?

@reiv
Copy link
Contributor Author

reiv commented Apr 5, 2018

Refactor for code style

Preview: documentation | landing | table

@giladgray
Copy link
Contributor

let's avoid pt-anything because we're actually going to change the prefix in 3.0 (hence #2325).

the main reason <Icon> doesn't live in the icons package is that it uses Intent in its props 😅

}),
);
paths[icon.iconName] = `[${pathStrings.join(",\n")}]`;
return paths;
Copy link
Contributor

Choose a reason for hiding this comment

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

something weird going on with whitespace here?

@reiv
Copy link
Contributor Author

reiv commented Apr 5, 2018

@giladgray it'll be hard to pursue the individual-icons-as-Components idea if <Icon> lives in core because of that circular dependency (TS imports are sequential AFAIK). Is there a workaround for that so we can reference <Icon> from within the icons package?

@reiv
Copy link
Contributor Author

reiv commented Apr 5, 2018

Fix whitespace

Preview: documentation | landing | table

@reiv
Copy link
Contributor Author

reiv commented Apr 6, 2018

A few more thoughts following up on the above:

  • In my opinion the most elegant solution would be to define <Icon> in the icons package (probably as it should have been were it not for its dependency on certain props and functions defined in core). The question is how to avoid the circular dependency so that only core is dependent on icons and not vice-versa. The following things from core are required by Icon:

    • IProps and IIntentProps interfaces, and by extension, the Intent enum
    • Classes.Icon string constant
    • Classes.intentClass function

    It's not much, so it could probably be duplicated in icons (types have no cost in compile size anyway). The alternative to duplication would be to define these in a separate commons package that's used by both core and icons, but I don't think that's a good idea, simply because it'd require consumers to import yet another package.

  • Another way would perhaps be to embrace the circular dependency and make it possible to import stuff from core in icons. Currently this is failing because the d.ts declarations in core/lib import types from "@blueprintjs/icons", which resolves to icons/lib. At this point in compilation, icons/lib either doesn't exist yet, or it does. Both are a problem. If it doesn't exist, tsc will abort with Cannot find module '@blueprintjs/icons'. If it does, tsc will complain about overwriting the declarations in icons/lib because those are now treated as "inputs" due to being referenced in core (AFAIK there is no compiler flag to force overwriting). Either way, I suppose we would need a way to "bootstrap" the icons package so that core can import the needed types in a way that doesn't interfere with actually compiling the icons package.

I'll probably try the former first and see where it takes us (can always revert if it's a no-go.)

Copy link
Contributor

@giladgray giladgray left a comment

Choose a reason for hiding this comment

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

looking good! i'm gonna have to check this out locally and play with it before final sign-off.

@@ -0,0 +1,71 @@
/*
* Copyright 2015 Palantir Technologies, Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

please update all copyrights to 2018

.then(({ data }) => data.match(/ d="[^"]+"/g) || [])
.then(paths => paths.map(s => s.slice(3)));
paths[icon.iconName] = `[${pathStrings.join(",\n")}]`;
return paths;
Copy link
Contributor

Choose a reason for hiding this comment

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

indentation?

public static displayName = "Blueprint2.Icon";

public static readonly SIZE_STANDARD = 16;
public static readonly SIZE_LARGE = 20;
Copy link
Contributor

Choose a reason for hiding this comment

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

reference the SVGIcon constants instead of redeclaring constants

@@ -26,6 +26,11 @@ export const HOTKEYS_WARN_DECORATOR_NO_METHOD = ns + ` @HotkeysTarget-decorated
export const HOTKEYS_WARN_DECORATOR_NEEDS_REACT_ELEMENT =
ns + ` "@HotkeysTarget-decorated components must return a single JSX.Element or an empty render.`;

export const ICON_STRING_NAMES_NOT_SUPPORTED =
Copy link
Contributor

Choose a reason for hiding this comment

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

is this used in core package? pretty sure it's safe to delete as it's defined in icons below.


let allIcons: { [key: string]: IconPartial | undefined } | null = null;

if (!(global as any).BLUEPRINT_ICONS_TREE_SHAKING) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 if a user never imports this component and only uses <NamedIcon /> exports, then will this file (and allIcons) be tree-shook automatically? as in, do we actually need this flag or can we rely on the build tool to detect that this module is unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But Icon is still used under the hood in other components, so it's unlikely that it's never imported.

@giladgray
Copy link
Contributor

@reiv take a peek at #2394: i wrote blueprint-icon-components TSLint rule to allow users to enforce component-only usage to enable easy tree shaking. does that seem sufficient? might be able to remove the global flag you've added in favor of enabling that lint rule?

@reiv
Copy link
Contributor Author

reiv commented Apr 17, 2018

@giladgray I'll try to address your code comments later today. As for that global flag, I'm quite certain it's still needed. The default behavior is to pull in all icon components when importing Icon.tsx, in order to continue supporting the use of strings in the icon prop (we need a mapping of name strings to actual icon components). Doing this would disable tree-shaking regardless of whether the user actually uses the string form or goes the icon component route -- hence the need to wrap the thing up in an if block which is then statically removed by dead code elimination at compile time. It's not pretty, but I don't think there's another way without breaking backwards compatibility. If anyone can think of an alternative mechanism that works without a compile flag, please do let me know.

Edit: lint rule looks good by the way. An auto-fixer would be the icing on top of course!

@reiv
Copy link
Contributor Author

reiv commented Apr 17, 2018

Move component-specific props out of common

Preview: documentation | landing | table

@reiv
Copy link
Contributor Author

reiv commented Apr 29, 2018

@giladgray Just letting you know that I won't have the time to work on this PR for the next few weeks due to other commitments. Sorry about that! Please feel free to take over from here (or cherry-pick into your own branch).

@giladgray
Copy link
Contributor

@reiv thanks for the update! i'm sorry for not having had the time to properly review this, but i will certainly make sure it gets released soon, likely as part of 3.0

@jonavila
Copy link

@giladgray Any progress with this PR?

@giladgray
Copy link
Contributor

@jonavila nope it's not a priority for us internally (we have bigger fish to tree-shake) and it can be done in a non-breaking way so not critical for 3.0.0. there may be some progress soon 😄

@ghost
Copy link

ghost commented Jun 21, 2018

What do you mean with we have bigger fish to tree-shake?

@giladgray
Copy link
Contributor

giladgray commented Jun 21, 2018

@jonavila we as a company use the icons widely and frequently in a parameterized way (hence the IconName string literals), so we can't actually tree shake any of them. as a result this feature doesn't have much direct value to Palantir, so it's low on our priority list.

@jonavila
Copy link

@giladgray Totally get that. I think your answer was directed to @johnunclesam

@giladgray
Copy link
Contributor

ah yep it was, thanks 🌴

@ghost
Copy link

ghost commented Oct 9, 2018

Breaking change

@giladgray
Copy link
Contributor

gonna close this PR because woo boy is it out of date!

@giladgray giladgray closed this Jan 18, 2019
@frederikhors
Copy link

gonna close this PR because woo boy is it out of date!

It is out of date, ok, @giladgray, but is tree-shaking for SVG icons a thing in blueprint today?

@giladgray
Copy link
Contributor

@frederikhors nope, sorry. this feature is sadly not a priority for the team.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A way to only bundle icons you actually use
5 participants