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

A way to only bundle icons you actually use #2193

Open
ConneXNL opened this issue Mar 2, 2018 · 19 comments
Open

A way to only bundle icons you actually use #2193

ConneXNL opened this issue Mar 2, 2018 · 19 comments

Comments

@ConneXNL
Copy link

@ConneXNL ConneXNL commented Mar 2, 2018

Is there currently no way to only bundle the icons you are actually using?

When importing the Icon Component:
import { Popover } from '@blueprintjs/core';

It adds a whopping ~350kb to your build because the entire iconSVGPaths.js is included. Tree shaking with Webpack 4 doesn't fix this.

Is this intended? Are there any workarounds to only bundle icons actually used?

@adidahiya

This comment has been minimized.

Copy link
Member

@adidahiya adidahiya commented Mar 2, 2018

This is intended in 2.0, yes. We considered a few different architectures for SVG icons and decided on the current one for the best upgrade story (sorry we didn't share the RFCs for @blueprintjs/icons externally; we'll try to do so in the future).

I would be open to ideas for changing the architecture if you can get webpack 4 tree shaking to work in to eliminate unused icons from your bundle -- there is still some time for breaking changes because we haven't released 2.0.0 yet.

@reiv

This comment has been minimized.

Copy link
Contributor

@reiv reiv commented Apr 1, 2018

I've been thinking about this a bit, and I think this is an approach that could work:

The idea would be to split up the icons into separate files; each file would be named after the icon and export just the path data needed to render that specific icon, and the icon name, at the module level.

// asterisk.ts
export name = "asterisk";
export path16 = ["M14.54 11.18l.01-.02L9.8 ..."]
export path20 = ["M18.52 14.171.01-.02L11.89 ..."]

Such a module could be described by an interface, like:

export interface IconDefinition {
    /** Icon description for a11y */
    name: string;
    /** SVG paths */
    path16: string[];
    path20: string[];
}

These icons could live in the icons package. A separate all.ts would simply be a re-export of every one of these individual files.

The Icon component in @blueprintjs/core would need to be expanded to also accept IconDefinition:

export interface IIconProps extends IIntentProps, IProps {
    ...
    icon: IconName | IconDefinition | JSX.Element | false | null | undefined;
    ...
    

To use the icon, one would import the file directly, e.g. import * as Asterisk from "@blueprintjs/icons/asterisk"; for convenience, one could write a dedicated icons.ts which would just export all icons used in that project.

Finally, there would need to be a way to prevent the entirety of the icons package from being required by default (I believe this happens as soon as Icon is imported, either directly or as part of a component that uses it). One way might be to wrap the import in an if expression that checks for some variable being set, like so:

// in Icon.tsx

import * as _allIcons from "@blueprintjs/icons/all";
let allIcons: typeof _allIcons | undefined = undefined;

if (!BLUEPRINT_INDIVIDUAL_ICONS) {
    allIcons = require("@blueprintjs/icons/all");
}

I believe this is enough for Webpack's tree shaking to kick in. The variable could either be global or added by Webpack's DefinePlugin.

Of course, not pulling in all icons at once means that the IconName form for the icon prop can no longer be used, so a runtime error should be issued if icon is specified as a string.

For maximum convenience, a Babel plugin could automagically transform string-form icon props into the corresponding imports (this is how lodash does it, for example.)

Since all this is purely additive, there wouldn't be any breaking changes.

Would love to hear your thoughts on this.

@reiv

This comment has been minimized.

Copy link
Contributor

@reiv reiv commented Apr 3, 2018

Perhaps splitting up the icons into individual files is a little overkill (it does seem to add a lot of overhead for Webpack too!) -- just providing the icons as individual exports rather than in continuous arrays would probably do the trick as well. Still trying things out on my end, but I'll PR as soon as I have something workable.

@giladgray

This comment has been minimized.

Copy link
Contributor

@giladgray giladgray commented Jun 20, 2018

this does not require API breaks so it will not happen in 3.0.0

@parisholley

This comment has been minimized.

Copy link

@parisholley parisholley commented Aug 19, 2018

@ConneXNL I worked around this in webpack by swapping the file with a custom version that only includes the icons I need:

new webpack.NormalModuleReplacementPlugin(
        /iconSvgPaths.js/,
        `${root}/iconSvgPaths.js`
)
@Ameobea

This comment has been minimized.

Copy link

@Ameobea Ameobea commented Aug 21, 2018

@parisholley I've tried this on my own project, but I wasn't able to get it working. I've tried tweaking the regex to /.*iconSvgPaths.[jt]s/ in an attempt to match correctly, but nothing seems to be working.

Did you make any other changes to your webpack conf to make this work?

@parisholley

This comment has been minimized.

Copy link

@parisholley parisholley commented Aug 23, 2018

@Ameobea nope, just follow w/e instructions exist for the webpack version you are using

https://webpack.js.org/plugins/normal-module-replacement-plugin/

i'm on the latest everything atm

@crysehillmes

This comment has been minimized.

Copy link

@crysehillmes crysehillmes commented Aug 27, 2018

@Ameobea Use /.*\/generated\/iconSvgPaths.*/.

@jasonbarry

This comment has been minimized.

Copy link

@jasonbarry jasonbarry commented Sep 3, 2018

Here's how I got it working with a next.js project. Also had to install file-loader for the build to succeed. Main bundle is 211KB smaller now.

// /next.config.js
const path = require('path');
const { NormalModuleReplacementPlugin } = require('webpack');
// ...
config.plugins.push(
  new NormalModuleReplacementPlugin(
    /.*\/generated\/iconSvgPaths.*/,
    path.resolve(__dirname, 'test/emptyModule.js'),
  )
)
// /test/emptyModule.js
export default '';
@noahjohn9259

This comment has been minimized.

Copy link

@noahjohn9259 noahjohn9259 commented Dec 18, 2018

@jasonbarry I got this error. Did you encounter this error?

Attempted import error: 'IconSvgPaths16' is not exported from './generated/iconSvgPaths'.
@ernestofreyreg

This comment has been minimized.

Copy link

@ernestofreyreg ernestofreyreg commented Jan 11, 2019

Solved for nextjs by:

// /next.config.js
const path = require('path')
const { NormalModuleReplacementPlugin } = require('webpack')

module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
    config.plugins.push(
      new NormalModuleReplacementPlugin(
        /.*\/generated\/iconSvgPaths.*/,
        path.resolve(__dirname, 'lib/icons.js'),
      )
    )

    return config
  }
}
// /lib/icons.js

export const IconSvgPaths16 = {
    "cross": ["M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L9.41 8z"],
    "lock": ["M13.96 7H12V3.95C12 1.77 10.21 0 8 0S4 1.77 4 3.95V7H1.96c-.55 0-.96.35-.96.9v6.91c0 .54.41 1.19.96 1.19h12c.55 0 1.04-.65 1.04-1.19V7.9c0-.55-.49-.9-1.04-.9zM6 7V3.95c0-1.09.9-1.97 2-1.97s2 .88 2 1.97V7H6z"],
    "user": ["M7.99-.01A7.998 7.998 0 0 0 .03 8.77c.01.09.03.18.04.28.02.15.04.31.07.47.02.11.05.22.08.34.03.13.06.26.1.38.04.12.08.25.12.37.04.11.08.21.12.32a6.583 6.583 0 0 0 .3.65c.07.14.14.27.22.4.04.07.08.13.12.2l.27.42.1.13a7.973 7.973 0 0 0 3.83 2.82c.03.01.05.02.07.03.37.12.75.22 1.14.29l.2.03c.39.06.79.1 1.2.1s.81-.04 1.2-.1l.2-.03c.39-.07.77-.16 1.14-.29.03-.01.05-.02.07-.03a8.037 8.037 0 0 0 3.83-2.82c.03-.04.06-.08.09-.13.1-.14.19-.28.28-.42.04-.07.08-.13.12-.2.08-.13.15-.27.22-.41.04-.08.08-.17.12-.26.06-.13.11-.26.17-.39.04-.1.08-.21.12-.32.04-.12.08-.24.12-.37.04-.13.07-.25.1-.38.03-.11.06-.22.08-.34.03-.16.05-.31.07-.47.01-.09.03-.18.04-.28.02-.26.04-.51.04-.78-.03-4.41-3.61-7.99-8.03-7.99zm0 14.4c-1.98 0-3.75-.9-4.92-2.31.67-.36 1.49-.66 2.14-.95 1.16-.52 1.04-.84 1.08-1.27.01-.06.01-.11.01-.17-.41-.36-.74-.86-.96-1.44v-.01c0-.01-.01-.02-.01-.02-.05-.13-.09-.26-.12-.39-.28-.05-.44-.35-.5-.63-.06-.11-.18-.38-.15-.69.04-.41.2-.59.38-.67v-.06c0-.51.05-1.24.14-1.72.02-.13.05-.26.09-.39.17-.59.53-1.12 1.01-1.49.49-.38 1.19-.59 1.82-.59.62 0 1.32.2 1.82.59.48.37.84.9 1.01 1.49.04.13.07.26.09.4.09.48.14 1.21.14 1.72v.07c.18.08.33.26.37.66.03.31-.1.58-.16.69-.06.27-.21.57-.48.62-.03.13-.07.26-.12.38 0 .01-.01.04-.01.04-.21.57-.54 1.06-.94 1.42 0 .06.01.13.01.19.04.43-.12.75 1.05 1.27.65.29 1.47.6 2.14.95a6.415 6.415 0 0 1-4.93 2.31z"],
    "warning-sign": ["M15.84 13.5l.01-.01-7-12-.01.01c-.17-.3-.48-.5-.85-.5s-.67.2-.85.5l-.01-.01-7 12 .01.01c-.09.15-.15.31-.15.5 0 .55.45 1 1 1h14c.55 0 1-.45 1-1 0-.19-.06-.35-.15-.5zm-6.85-.51h-2v-2h2v2zm0-3h-2v-5h2v5z"],
}

export const IconSvgPaths20 = {
    "cross": ["M11.41 10l4.29-4.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L10 8.59l-4.29-4.3a1.003 1.003 0 0 0-1.42 1.42L8.59 10 4.3 14.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4.29-4.3 4.29 4.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L11.41 10z"],
    "lock": ["M15.93 9H14V4.99c0-2.21-1.79-4-4-4s-4 1.79-4 4V9H3.93c-.55 0-.93.44-.93.99v8c0 .55.38 1.01.93 1.01h12c.55 0 1.07-.46 1.07-1.01v-8c0-.55-.52-.99-1.07-.99zM8 9V4.99c0-1.1.9-2 2-2s2 .9 2 2V9H8z"],
    "user": ["M10 0C4.48 0 0 4.48 0 10c0 .33.02.65.05.97.01.12.03.23.05.35.03.2.05.4.09.59.03.14.06.28.1.42l.12.48c.05.16.1.31.15.46.05.13.09.27.15.4.06.16.13.32.21.48.05.11.1.22.16.33.09.17.17.34.27.5.05.09.1.17.15.25.11.18.22.35.34.52.04.06.08.11.12.17 1.19 1.62 2.85 2.86 4.78 3.53l.09.03c.46.15.93.27 1.42.36.08.01.17.03.25.04.49.07.99.12 1.5.12s1.01-.05 1.5-.12c.08-.01.17-.02.25-.04.49-.09.96-.21 1.42-.36l.09-.03c1.93-.67 3.59-1.91 4.78-3.53.04-.05.08-.1.12-.16.12-.17.23-.35.34-.53.05-.08.1-.16.15-.25.1-.17.19-.34.27-.51.05-.11.1-.21.15-.32.07-.16.14-.32.21-.49.05-.13.1-.26.14-.39.05-.15.11-.31.15-.46.05-.16.08-.32.12-.48.03-.14.07-.28.1-.42.04-.19.06-.39.09-.59.02-.12.04-.23.05-.35.05-.32.07-.64.07-.97 0-5.52-4.48-10-10-10zm0 18a7.94 7.94 0 0 1-6.15-2.89c.84-.44 1.86-.82 2.67-1.19 1.45-.65 1.3-1.05 1.35-1.59.01-.07.01-.14.01-.21-.51-.45-.93-1.08-1.2-1.8l-.01-.01c0-.01-.01-.02-.01-.03a4.42 4.42 0 0 1-.15-.48c-.33-.07-.53-.44-.61-.79-.08-.14-.23-.48-.2-.87.05-.51.26-.74.49-.83v-.08c0-.63.06-1.55.17-2.15.02-.17.06-.33.11-.5.21-.73.66-1.4 1.26-1.86.62-.47 1.5-.72 2.28-.72.78 0 1.65.25 2.27.73.6.46 1.05 1.12 1.26 1.86.05.16.08.33.11.5.11.6.17 1.51.17 2.15v.09c.22.1.42.33.46.82.04.39-.12.73-.2.87-.07.34-.27.71-.6.78-.04.16-.09.33-.15.48 0 .01-.02.05-.02.05-.26.71-.67 1.33-1.17 1.78 0 .08.01.16.01.23.05.54-.15.94 1.31 1.59.81.36 1.84.74 2.68 1.19A7.958 7.958 0 0 1 10 18z"],
    "warning-sign": ["M19.86 17.52l.01-.01-9-16-.01.01C10.69 1.21 10.37 1 10 1s-.69.21-.86.52l-.01-.01-9 16 .01.01c-.08.14-.14.3-.14.48 0 .55.45 1 1 1h18c.55 0 1-.45 1-1 0-.18-.06-.34-.14-.48zM11 17H9v-2h2v2zm0-3H9V6h2v8z"],
}

This way you can pick and choose the icons you actually use from the generated file.

@amazing-space-invader

This comment has been minimized.

Copy link

@amazing-space-invader amazing-space-invader commented Apr 1, 2019

Hey! Is there any temporary solution to completely exclude icons from the bundle in create-react-app without ejecting?

@Ameobea

This comment has been minimized.

Copy link

@Ameobea Ameobea commented Apr 1, 2019

@amazing-space-invader There are some hacky things you can do:

You can fork the blueprint repo, deleting all of the unused icons from the source code, and pull in the fork as a dependency instead.

Or you can just add a step to your build process that overwrites the icons file in your node_modules directory with a custom file that has all of the unused icons removed.

@dphilipson

This comment has been minimized.

Copy link

@dphilipson dphilipson commented Apr 1, 2019

@amazing-space-invader In addition to what @Ameobea said, there's a couple of libraries that allow you to mess with the create-react-app config without ejecting. I just got this icon bundling thing to work in my project using Craco, and it should work about the same in similar CRA config libraries like react-app-rewired.

To get it working with Craco, I followed the install instructions in the Craco readme, then added a craco.config.js with contents:

const path = require("path");
const { NormalModuleReplacementPlugin } = require("webpack");

module.exports = {
  webpack: {
    plugins: [
      new NormalModuleReplacementPlugin(
        /.*\/generated\/iconSvgPaths.js/,
        path.resolve(__dirname, "emptyIconSvgPaths.js"),
      ),
    ],
  },
};

and also added a file emptyIconSvgPaths.js with contents

export const IconSvgPaths16 = {};
export const IconSvgPaths20 = {};

You can see a working example repo here.

Disclaimer: messing with Create React App's config in this way is sketchy and may eventually cause you upgrade pain. Do so at your own risk.

@fredfortier

This comment has been minimized.

Copy link

@fredfortier fredfortier commented Jun 8, 2019

@dphilipson I don't feel educated enough about Craco to apply this example directly to the Blueprint. Do you (or anyone else) have an example of selectively importing Blueprint icons?

@dphilipson

This comment has been minimized.

Copy link

@dphilipson dphilipson commented Jun 11, 2019

@fredfortier I'll try to help if I can. First of all, Craco or similar libraries are only necessary if you are using the Create React App starter project. The steps for selectively importing icons depend on your project setup.

If you are using Create React App (and haven't ejected):

  1. Install Craco with npm install @craco/craco or yarn add @craco/craco.
  2. In your package.json, replace "react-scripts start", "react-scripts build", and "react-scripts test" with "craco start", "craco build" respectively.
  3. In your project root, create a file named craco.config.js with contents described in my previous post.
  4. Also in your project root, create a file named emptyIconSvgPaths.js with contents described in my previous post.

If you want an example of all these steps in practice, see this commit.

This will bundle the project with no icons. If you want to selectively add certain icons, then add them to emptyIconSvgPaths.js as described in @ernestofrereg's post.

Now if you're not using Create React App, then see if your project has a Webpack configuration file, likely in the project root and named webpack.config.js. Then edit this configuration, adding

      new NormalModuleReplacementPlugin(
        /.*\/generated\/iconSvgPaths.*/,
        path.resolve(__dirname, 'lib/icons.js'),
      )

to the list of plugins (also adding the necessary imports to the top of the file).

Or if you're using Next.js, then try following the instructions in @ernestofreyreg's post in their entirety.

@ernestofreyreg

This comment has been minimized.

Copy link

@ernestofreyreg ernestofreyreg commented Jun 12, 2019

You can also create your own icons. You can give it any name and just copy the SVG paths (adjusted for 20x20 and 16x16 viewbox)

@adidahiya adidahiya removed this from the next milestone Jun 26, 2019
@skovy

This comment has been minimized.

Copy link
Contributor

@skovy skovy commented Jul 24, 2019

I believe the approach @reiv mentioned above could be effective. The Font Awesome packages do something very similar (they also have docs specifically for tree shaking: https://fontawesome.com/how-to-use/with-the-api/other/tree-shaking)

They have two options for using. One, import the icon directly (import { faCoffee } from '@fortawesome/free-solid-svg-icons') which allows tree shaking because webpack can determine which exports are used and rely on minification using the Terser plugin to perform the dead code elimination. Or, if not using a tool that supports tree-shaking you can do a "deep import": import { faCoffee } from '@fortawesome/free-solid-svg-icons/faCoffee'.

The other option they provide is to add all the icons (equivalent to what's happening today):

import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'

// Add all icons to the library so you can use it in your page
library.add(fas, far, fab)

(Relevant docs: https://fontawesome.com/how-to-use/with-the-api/setup/importing-icons#icon-names)

Here is an example of what the esm source looks like: https://unpkg.com/@fortawesome/free-solid-svg-icons@5.9.0/index.es.js

Each individual icon is exported to enable the above tree shaking, but the _iconCache as fas value is also exported which includes all the icons so for those that are no interested in tree shaking it's still easy to use the library.add(fas) API.

@giladgray @adidahiya would you be open to a proof-of-concept PR with these changes?

Edit: oh wow, I missed the original attempt here: #2356 😅

@ashi009

This comment has been minimized.

Copy link

@ashi009 ashi009 commented Oct 23, 2019

Isn't the most straightforward solution is to export those paths directly without using the names? By dropping that layer of indirection, this can be done with just standard ES module semantic. Bundlers will drop unused icons as unused variables, without the help of special loaders or magic function calls.

@blueprint/icons/index.ts:

function define(name: string, path16: string[], path20: string[]): IconDefinition { return { name, path16, path20 }; }

export const SOME_ICON = /*#__PURE__*/define("some_icon", [...], [...]);
export const SOME_UNUSED_ICON = /*#__PURE__*/define("some_unused_icon", [...], [...]);

some.tsx:

import * as icons from '@blueprint/icons';

<Icon icon={icons.SOME_ICON} />
jumpinjackie added a commit to jumpinjackie/mapguide-react-layout that referenced this issue Nov 7, 2019
…blueprint's icon package and replace it with our own slimmed down module instead, allowing us to not bundle 300+kb of SVG icons we don't actually use.

Ref: palantir/blueprint#2193 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.