React-md: [General] - Using modules does not exclude unused components as expected

Created on 12 Oct 2017  路  16Comments  路  Source: mlaursen/react-md

I guess something is wrong with the rollup.config.js file. When doing:

import { Button } from 'react-md';

it looks like the full library is still being imported. The bundle size does not change even when more components are imported.

pasted image at 2017_10_12 09_16 am

bug help wanted

Most helpful comment

yes @stickfigure. That trick with the babel-plugin-transform-imports and of course a good code splitting. In my case, in development my bundle size was ~8.5 MB due to source maps. Besides those large bundles I had some _app_ modules inside them so every time I saved a file the bundle was compiled.

ls -lh node_modules/react-md/es/react-md.js gives me

-rw-r--r-- 1 hisa staff 1.4M Dec 4 13:47 node_modules/react-md/es/react-md.js

I think the react-md build should be an index file that exports each of the components and not a bundle containing all the lib's modules.

A library with tree-shaking working ok is "react-router": "^3.2.0",.

import { Route, IndexRoute } from "react-router";

results in a smaller bundle size than

import Route from 'react-router/lib/Route';
import IndexRoute from 'react-router/lib/IndexRoute';

With the tricks mentioned my vendor-base.js bundle is now 5.91MB ... it is the largest in my output but it only contains vendor code so it doesn't need to be updated each time a save a file in my app.

All 16 comments

I am also looking for a solution to this problem. Maybe someone has an idea!

Yeah, I'm not sure what to do. I have an update where my build is similar to react-router's, but it still looks like the unused code is being bundled for both. /shrug

Hey guys, I'm struggling with my bundle size too. A temporary solution I found is to use babel-plugin-transform-imports ... that way you can use the namespaced imports and the plugin will take care of transforming them to deep imports considerably reducing the bundle size.

For my current components I'm using the following transform function which you might like to extend according to your needs:

module.exports = function transformImports(importName) {
  const reactMD = "react-md/lib";

  switch (importName) {
    // Cards
    case "CardActions":
    case "CardText":
    case "CardTitle":
      return `${reactMD}/Cards/${importName}`;

    // DataTables
    case "TableHeader":
    case "TableCardHeader":
    case "TableBody":
    case "TableRow":
    case "TableColumn":
      return `${reactMD}/DataTables/${importName}`;

    // Inks
    case "injectInk":
      return `${reactMD}/Inks/${importName}`;

    // Tabs
    case "TabsContainer":
    case "Tabs":
    case "Tab":
      return `${reactMD}/Tabs/${importName}`;

    // Lists
    case "ListItem":
      return `${reactMD}/Lists/${importName}`;

    // ExpansionPanels
    case "ExpansionList":
      return `${reactMD}/ExpansionPanels/${importName}`;

    // Dialogs
    case "DialogContainer":
      return `${reactMD}/Dialogs/${importName}`;

    // Pickers
    case "DatePicker":
      return `${reactMD}/Pickers/${importName}`;

    // Progress
    case "CircularProgress":
      return `${reactMD}/Progress/${importName}`;

    // SelectionControls
    case "Checkbox":
      return `${reactMD}/SelectionControls/${importName}`;

    default:
      return `${reactMD}/${importName}s/${importName}`;
  }
};

@hisapy I stumbled across the same thing today but didn't investigate any further but also wanted to build a custom import transformer for react-md. Will grab your code and extend it further to my needs, thanks!

Currently react-md is the largest dependency regarding bundle size (~1.25MB).

I got to a point where waiting for re-compiling and reloading the page was a PITA due to large bundles. Now I'm back on track ... the good stuff about this is that once the tree-shaking stuff is fixed we can simply remove the plugin from our webpack config

Do I understand this correctly: If I switch all my import { List } from 'react-md'; lines to import List from 'react-md/Lists/List' (which is what IntelliJ tries to import in typescript), I might be able to cut down on the bundle size and the time webpack takes to compile my app?

These 30s developer webpack builds are killing me. It used to be fast, but around the time react-md switched to rollup the build time went haywire.

yes @stickfigure. That trick with the babel-plugin-transform-imports and of course a good code splitting. In my case, in development my bundle size was ~8.5 MB due to source maps. Besides those large bundles I had some _app_ modules inside them so every time I saved a file the bundle was compiled.

ls -lh node_modules/react-md/es/react-md.js gives me

-rw-r--r-- 1 hisa staff 1.4M Dec 4 13:47 node_modules/react-md/es/react-md.js

I think the react-md build should be an index file that exports each of the components and not a bundle containing all the lib's modules.

A library with tree-shaking working ok is "react-router": "^3.2.0",.

import { Route, IndexRoute } from "react-router";

results in a smaller bundle size than

import Route from 'react-router/lib/Route';
import IndexRoute from 'react-router/lib/IndexRoute';

With the tricks mentioned my vendor-base.js bundle is now 5.91MB ... it is the largest in my output but it only contains vendor code so it doesn't need to be updated each time a save a file in my app.

I've created a small package with a transformer for most components: https://github.com/Kaishiyoku/webpack-react-md-import-transformer

Please read the installation instructions carefully.

IMHO I think that transforming imports is not actually _tree-shaking_, but overall, your lib looks good @Kaishiyoku

@hisapy You're right! I updated the README.

Thanks everyone! I replaced all the "react-md" imports with specific file imports and my build went from 30+s to sub 10s, sometimes even 5s. MUCH better. It was tedious to do by hand in a large project but the IDE did most of the work.

Cool @stickfigure ... I was doing that but since I'm a lazy developer I looked for a pluggable solution (like babel-plugin-lodash) so then I started writing my first babel plugin. Luckily, after some researching/reading I found the babel-plugin-transform-imports. Also, I remembered how tedious was rewriting the deep imports to namespaced imports when upgraded react-md to the version with tree-shaking

I'd suggest you try the https://github.com/Kaishiyoku/webpack-react-md-import-transformer created by @Kaishiyoku . In the future, when the tree-shaking gets fixed you'll just have to unplug the the plugin to try tree-shaking again.

unfortunately I'm using create-react-app so I can't seem to use the babel build.
maybe i should just go back to the individual import lines for now.

alas I can't figure out how to get it to work with my own setup, but in case anyone else is using hisapy's setup i put together a (i think) complete version of that switch statement:

module.exports = function transformImports(importName) {
  const reactMD = "react-md/lib";

  switch (importName) {
    // Autocompletes
    case "Autocomplete":
      return `${reactMD}/Autocompletes/${importName}`;
    // Avatars
    case "Avatar":
      return `${reactMD}/Avatars/${importName}`;
    // Badges
    case "Badge":
      return `${reactMD}/Badges/${importName}`;
    // Bottom Navigations
    case "BottomNav":
    case "BottomNavigation":
      return `${reactMD}/BottomNavigations/${importName}`;
    // Buttons
    case "Button":
    case "FlatButton":
    case "FloatingButton":
    case "IconButton":
    case "RaisedButton":
      return `${reactMD}/Buttons/${importName}`;
    // Cards
    case "Card":
    case "CardActionOverlay":
    case "CardActions":
    case "CardExpander":
    case "CardMedia":
    case "CardText":
    case "CardTitle":
    case "CardTitleBlock":
      return `${reactMD}/Cards/${importName}`;
    // Chips
    case "Chip":
      return `${reactMD}/Chips/${importName}`;
    // DataTables
    case "DataTable":
    case "DropdownMenuColumn":
    case "EditDialog":
    case "EditDialogColumn":
    case "MenuButtonColumn":
    case "SelectFieldColumn":
    case "TableBody":
    case "TableCardHeader":
    case "TableCheckbox":
    case "TableColumn":
    case "TableFooter":
    case "TableHeader":
    case "TablePagination":
    case "TableRow":
      return `${reactMD}/DataTables/${importName}`;
    // Dialogs
    case "Dialog":
    case "DialogContainer":
    case "DialogFooter":
    case "DialogTitle":
      return `${reactMD}/Dialogs/${importName}`;
    // Dividers
    case "Divider":
      return `${reactMD}/Dividers/${importName}`;
    // Drawers
    case "Drawer":
    case "DrawerTypes":
    case "Overlay":
      return `${reactMD}/Drawers/${importName}`;
    // ExpansionPanels
    case "ExpansionList":
    case "ExpansionPanel":
    case "PanelContent":
      return `${reactMD}/ExpansionPanels/${importName}`;
    // FABTransitions
    case "SpeedDial":
      return `${reactMD}/FABTransitions/${importName}`;
    // File Inputs
    case "FileInput":
    case "FileUpload":
      return `${reactMD}/FileInputs/${importName}`;
    // Font Icons
    case "FontIcon":
      return `${reactMD}/FontIcons/${importName}`;
    // Grids
    case "Cell":
    case "Grid":
    case "GridList":
      return `${reactMD}/Grids/${importName}`;
    // Helpers
    case "AccessibleFakeButton":
    case "AccessibleFakeInkedButton":
    case "Collapse":
    case "FocusContainer":
    case "HorizontalAnchors":
    case "IconSeperator":
    case "Layover":
    case "Portal":
    case "Positions":
    case "ResizeObserver":
    case "VerticalAnchors":
    case "anchorShape":
    case "fixedToShape":
    case "positionShape":
      return `${reactMD}/Helpers/${importName}`;
    // Inks
    case "Ink":
    case "InkContainer":
    case "injectInk":
      return `${reactMD}/Inks/${importName}`;
    // Lists
    case "List":
    case "ListItem":
    case "ListItemControl":
    case "ListItemText":
    case "TileAddon":
      return `${reactMD}/Lists/${importName}`;
    // Media
    case "Media":
    case "MediaOverlay":
      return `${reactMD}/Media/${importName}`;
    // Menus
    case "DropdownMenu":
    case "Menu":
    case "MenuButton":
      return `${reactMD}/Menus/${importName}`;
    // Navigation Drawers
    case "CloseButton":
    case "JumpToContentLink":
    case "MiniListItem":
    case "NavigationDrawer":
      return `${reactMD}/NavigationDrawers/${importName}`;
    // Papers
    case "Paper":
      return `${reactMD}/Papers/${importName}`;
    // Pickers
    case "CalendarDate":
    case "CalendarHeader":
    case "CalendarMonth":
    case "ClockFace":
    case "ClockHand":
    case "ClockTime":
    case "DatePicker":
    case "DatePickerCalendar":
    case "DatePickerContainer":
    case "DatePickerHeader":
    case "PickerControl":
    case "PickerFooter":
    case "TimePeriods":
    case "TimePicker":
    case "TimePickerContainer":
    case "TimePickerHeader":
    case "Year":
    case "YearPicker":
      return `${reactMD}/Pickers/${importName}`;
    // Progress
    case "CircularProgress":
    case "LinearProgress":
      return `${reactMD}/Progress/${importName}`;
    // Select Fields
    case "SelectField":
    case "SelectFieldInput":
    case "SelectFieldToggle":
      return `${reactMD}/SelectFields/${importName}`;
    // SelectionControls
    case "Checkbox":
    case "Radio":
    case "RadioGroup":
    case "SelectionControl":
    case "SelectionControlGroup":
    case "Switch":
    case "SwitchThumb":
    case "SwitchTrack":
      return `${reactMD}/SelectionControls/${importName}`;
    // Sidebars
    case "Sidebar":
      return `${reactMD}/Sidebars/${importName}`;
    // Sliders
    case "DiscreteValue":
    case "Slider":
    case "SliderLabel":
    case "Thumb":
    case "ThumbMask":
    case "Track":
    case "TrackFill":
      return `${reactMD}/Sliders/${importName}`;
    // Snackbars
    case "Snackbar":
    case "SnackbarContainer":
      return `${reactMD}/Snackbars/${importName}`;
    // Subheaders
    case "Subheader":
      return `${reactMD}/Subheaders/${importName}`;
    // SVG Icons
    case "SVGIcon":
      return `${reactMD}/SVGIcons/${importName}`;
    // Tabs
    case "MenuTab":
    case "Tab":
    case "TabIndicator":
    case "TabOverflowButton":
    case "TabPanel":
    case "Tabs":
    case "TabsContainer":
      return `${reactMD}/Tabs/${importName}`;
    // Text Fields
    case "FloatingLabel":
    case "InputField":
    case "Message":
    case "PasswordButton":
    case "TextArea":
    case "TextField":
    case "TextFieldDivider":
    case "TextFieldMessage":
      return `${reactMD}/TextFields/${importName}`;
    // Toolbars
    case "Toolbar":
    case "ToolbarTitle":
      return `${reactMD}/Toolbars/${importName}`;
    // Tooltips
    case "Tooltip":
    case "TooltipContainer":
    case "Tooltipped":
    case "injectTooltip":
      return `${reactMD}/Tooltips/${importName}`;
    // constants
    case "CSSTransitionGroupTick":
    case "additionalInkTriggerKeys":
    case "keyCodes":
    case "media":
      return `${reactMD}/constants/${importName}`;
    default:
      return `${reactMD}/${importName}s/${importName}`;
  }
};

Hey devs ..

I just wanted to post an update with perhaps valid info regarding this issue

image

The screenshot is from an app I've just created with:

  • CRA 1.3.0
  • react-scripts 2.1.1
  • react-md 1.9.0

    The only import from react-md is

import { NavigationDrawer } from "react-md";

So we can confirm that the tree-shaking is still not working. Following are the stats of react-md in the bundle:

  • Stat size: 1.28 MB
  • Parsed size: 485.95 KB
  • Gzipped size: 97.05 KB

Looking at this stats, imagine the reduction with can achieve with _dead code elimination_.

See webpack-bundle-analyzer Size definitions for more info.

It looks like adding sideEffects to the package.json will make tree shaking work with Webpack, according to https://webpack.js.org/guides/tree-shaking#mark-the-file-as-side-effect-free and to some tests I made.

I'm not sure though if react-md has some "effectful" files that need to be excluded.

Was this page helpful?
0 / 5 - 0 ratings