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

RFC: remove production propTypes, allow small custom builds #524

Closed
45 of 47 tasks
layershifter opened this issue Sep 21, 2016 · 32 comments
Closed
45 of 47 tasks

RFC: remove production propTypes, allow small custom builds #524

layershifter opened this issue Sep 21, 2016 · 32 comments

Comments

@layershifter
Copy link
Member

layershifter commented Sep 21, 2016

I've builded stardust with webpack in my project.

There are two problems:

  • unused components in bundle that increases bundle size;
  • PropTypes are in production bundle, too.

bundlers TODO

propTypes update

@layershifter layershifter changed the title Proptypes in bundle Question: Proptypes in bundle Sep 21, 2016
@levithomason
Copy link
Member

Agreed on both concerns. Removing propTypes in production and allowing smaller builds. Here are my thoughts.

propTypes

There are transforms to remove propTypes in production builds, however, we're currently using them in getUnhandledProps to determine which props the component does not support. Removing them will break things. We'll need to remove library use of propTypes.

Unused Components

In the optimization phase (after getting the bulk of work completed) I plan to publish individual components. This way users can just install what they need.

We should also consider structuring our primary published project to users can do something similar to:

import Button from 'stardust/Button'

If we did this, we could also provide a babel plugin like lodash has done with babel-plugin-lodash. It looks at usages of lodash and transforms the imports to only import what you use, drastically reducing build size. We use it on the doc site.

@levithomason levithomason changed the title Question: Proptypes in bundle RFC: remove production propTypes, allow small custom builds Sep 21, 2016
@layershifter
Copy link
Member Author

layershifter commented Sep 23, 2016

propTypes

There are no change that we can remove getUnhandledProps at now and near future, so we need use it 😋 I don't read about Facebook's plans about propTypes, but I think there are no chance for their complete removal.

I'm thinking about Babel plugin that will run following transform:

Component.propTypes = {
  className: PropTypes.string,
  content: PropTypes.string,
  header: customPropTypes.every(),
  icon: customPropTypes.every()
}

Component.handledProps = ['className', 'content', 'header', 'icon']

So, we can modify getUnhandledProps to

if(process.env.NODE_ENV === 'production') {
 const handledProps = Component.handledProps
} else {
 const handledProps = _.union(
    Component.autoControlledProps,
    _.keys(Component.defaultProps),
    _.keys(Component.propTypes),
  )
}

That might be splited on build to:

const handledProps = Component.handledProps

Thoughts?

@levithomason
Copy link
Member

If we added this, I think we should use https://nkt.github.io/babel-plugin-react-remove-prop-types. This package strips them entirely. In that case, I think we should update getUnhandledProps to be more functional, and not rely on external references. It should take in an array of 'handledProps'.

I know it would be more code to write, however, the current function is weak in that it has external dependencies and makes assumptions about those dependencies. Currently, it takes in a Component and checks for 3 different props definitions. That would be replaced by a single hard coded array.

This would make it faster, more reliable/reusable, and we could use the community leader for removing propTypes.

Thoughts?

@layershifter
Copy link
Member Author

If we will use handcoded handledProps, it will be easy implement. We can make it after migration to semantic-react, because it looks absurdly in production build.

_376

@levithomason
Copy link
Member

We also need to remove the index files from each component "type" folder. Otherwise, we'll automatically bundle extra components. See https://github.com/TechnologyAdvice/stardust/pull/570#discussion_r81445116.

Lastly, the common tests for exported components needs updated. The failure message instructs users to export their components in those index files. Once we remove them, that message is no longer valid. It should instead instruct users to export their components in the main index file.

@besh
Copy link

besh commented Oct 3, 2016

Do you guys see anything wrong with doing this in a lib/stardust file in our project?

export { default as Button } from 'stardust/dist/commonjs/elements/Button/Button';

Then when required, we can just do

import { Button } from 'lib/stardust';

This works for our needs but I wasn't sure if there were any issues we might encounter doing this (other than breaking changes as you guys make updates)?

@layershifter
Copy link
Member Author

PropTypes

As we decided in #684:

  • we need separate enums from Component _meta;
  • _meta.props is array of handled props by Component.

I've sketched some code there, if we approve it I'll open new PR for this.

Stateless component

function SUIComponent () {}

SUIComponent._meta = {
  // ...
  props: [
    'as',
    'children',
    'size',
    // etc.
  ]
}

if (process.env.NODE_ENV !== 'production') {
  SUIComponent.propValues = {
    size: SUI.SIZES,
  }

  SUIComponent.propTypes = {
    as: customProptypes.as,
    size: PropTypes.oneOf(SUIComponent.propValues.size),
  }
}

propValues - I really don't like this idea, but it's simpliest way to get values for commentTest.

@levithomason
Copy link
Member

levithomason commented Oct 18, 2016

To leave room for future expansion of our prop definition (component explorer), we can use an object for each prop. I've also renamed it from propValues to props:

function SUIComponent() {}

SUIComponent._meta = {
  props: ['as', 'children', 'size']
}

if (process.env.NODE_ENV !== 'production') {
  SUIComponent.props = {
    size: {
      values: SUI.SIZES,
    },
  }

  SUIComponent.propTypes = {
    as: customProptypes.as,
    size: PropTypes.oneOf(SUIComponent.props.size.values),
  }
}

It is worth noting that we'll need to pull propTypes out of classes so we can wrap them in the production check as well:

class SUIComponent extends Component {
  _meta = {
    props: ['as', 'children', 'size']
  }
}

if (process.env.NODE_ENV !== 'production') {
  SUIComponent.props = {
    size: {
      values: SUI.SIZES,
    },
  }

  SUIComponent.propTypes = {
    as: customProptypes.as,
    size: PropTypes.oneOf(SUIComponent.props.size.values),
  }
}

Lastly, for the first PR can we update the tests, utilities, contributing guide, and one component only? This way it is a smaller PR that we can review quickly.

@layershifter
Copy link
Member Author

layershifter commented Jan 13, 2017

Okay, #731 was merged, wow ✌️ However, we need to do cleanup work to make it work properly. You can follow #1155 as example.

@layershifter
Copy link
Member Author

layershifter commented Feb 21, 2017

Okay, all components where updated 🖖


Also, @levithomason I think that we need to add page called Optimization to docs where will be descibed hot to optimize bundle size:

  • remove propTypes in production build;
  • how use babel-plugin-lodash for commonjs modules;
  • notes about es modules support.

@levithomason
Copy link
Member

Regarding the docs page, I completely agree. I'd like to have some docs on basic setup as well. This has been discussed in another issue that I cannot find ATM. Starting build/optimize docs anywhere we can for now is good by me. We can iterate afterward.

@layershifter
Copy link
Member Author

I've played with rollup, treeshaking doesn't work there.
I'll try webpack 2 today.

@layershifter
Copy link
Member Author

Same problem with webpack 😕

@levithomason
Copy link
Member

Depends on how they are being imported as well. Could you open a PR and add your setup to the repo for review? I'd like to start some minimal project setup examples in our repo anyhow. Let's put them in /examples in the root of the repo. This way we can link to them from the docs, too.

@pke
Copy link

pke commented Mar 6, 2017

Appreciate the efforts to reduce build sizes.
How about removing the full lodash imports if all u need from the lib is _.isNil()? This also would dramatically reduce build times&sizes.

@jeffcarbs
Copy link
Member

@pke - See this comment: #830 (comment)

We have this done automatically by babel using https://github.com/lodash/babel-plugin-lodash. Compare the src and dist directories to see it in action.

@pke
Copy link

pke commented Mar 6, 2017

@jcarbo Ah I see. The bundle is still a heavy 500kb. I am afraid to use it in my app which is already over 1MB. And with webpack 1 there is no tree shaking.

@jeffcarbs
Copy link
Member

@pke - Despite the name, you can actually use the babel-plugin-lodash package to cherry-pick any library. See this comment (and subsequent discussion) where I explained how to do it with SUI-React: #731 (comment)

It will essentially transform:

// from
import { Button } from 'semantic-ui-react'

// to
import Button from 'semantic-ui-react/dist/commonjs/elements/Button'

Bundle size is definitely something we're trying to solve with issues/PRs like these.

@levithomason
Copy link
Member

@pke

The bundle is still a heavy 500kb.

The full library in UMD format is only 81.8 kB minified and gzipped. You can see bundle sizes for the full library, as well as every component individually, in #1233.

You should have slightly smaller builds assuming you aren't building to UMD and likely do not need to include the UMD checks for all environments.

How about removing the full lodash imports if all u need from the lib is _.isNil()?

To clarify, the full list of lodash methods currently used is quite large. It also spans lodash and lodash/fp. I'd liket to eventually migrate everything to lodash/fp, though, for now, these are the methods in use:

_.assign
_.camelCase
_.capitalize
_.castArray
_.clamp
_.compact
_.curry
_.debounce
_.difference
_.dropRight
_.each
_.endsWith
_.eq
_.escapeRegExp
_.every
_.filter
_.find
_.findIndex
_.flatMap
_.flow
_.get
_.has
_.head
_.inRange
_.includes
_.indexOf
_.intersection
_.invoke
_.isArray
_.isEmpty
_.isEqual
_.isFunction
_.isNil
_.isNumber
_.isObject
_.isPlainObject
_.isString
_.isUndefined
_.kebabCase
_.keys
_.last
_.map
_.mapValues
_.max
_.min
_.noop
_.omit
_.partialRight
_.pick
_.random
_.range
_.reduce
_.round
_.sample
_.snakeCase
_.some
_.sortBy
_.startCase
_.startsWith
_.sum
_.tail
_.take
_.times
_.transform
_.trim
_.union
_.uniq
_.values
_.without

@pke
Copy link

pke commented Mar 6, 2017

Wow, thanks for the the thorough responses, guys.
Maybe I'll give it another try. Gzipped sizes don't matter for Cordova apps though. I am not worried about data that goes over the wire, since the script bundle is already on the mobile device. However I want to load and parse it as fast as possible. And for that an additional gzip deflate increases load time.

But minified might help to decrease load time and parsing.

The additional transformation @jcarbo mentioned might also help. I just wish that we would be able to import Button from 'semantic-ui-react/lib/elements/Button right in my ES6 code.

@levithomason
Copy link
Member

I just wish that we would be able to import Button from 'semantic-ui-react/lib/elements/Button right in my ES6 code.

You certainly can if you compile it yourself, just pull it form our src 😄

import Button from 'semantic-ui-react/src/elements/Button'

Or, you can use the precompiled commonjs module:

import Button from 'semantic-ui-react/dist/commonjs/elements/Button'

Or, you can use the precompiled ES build, which preserves import statements for tree shaking:

import Button from 'semantic-ui-react/dist/es/elements/Button'

@levithomason
Copy link
Member

@layershifter is this issue actually complete now? The last check list item is:

  • unused components (I'm realy don't use Rail in app) in bundle that increases bundle size;

However, we offer several ways to do this now with babel-plugin-lodash, tree shaking with the ES build, and direct component imports (as above).

Let me know if you had any other needs or ideas in mind for this.

@layershifter
Copy link
Member Author

layershifter commented Mar 12, 2017

No, I want to check this TODO (#524 (comment)) before we finally close this.

@levithomason
Copy link
Member

Sounds good, although, it is not clear to me what other ways of removing unused components we could offer, short of #1443.

@henri-hulski
Copy link

Tree-shaking with webpack2 doesn't work for me.
Do I need babel-plugin-lodash also for webpack2?

@levithomason
Copy link
Member

You do not. Are you importing from the ES build target?

import { Button} from 'semantic-ui-react/dist/es'

Tree shaking can only work with ES import statements, the default import location provides commonjs modules.

@henri-hulski
Copy link

No from semantic-ui-react.
But I changed it now and nothing changed.

@henri-hulski
Copy link

I check it again, when I import from semantic-ui-react it automatically imports from semantic-ui-react/dist/es.
This is how it looks:
bildschirmfoto von 2017-03-25 16 39 30

@levithomason
Copy link
Member

We're building through a webpack 2 and tree example. Let's address any issues there, #1497.

@layershifter
Copy link
Member Author

We have separate issues for webpack 2 and rollup, so I'm closing this issue for housekeeping because technically we reached its goal.

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

No branches or pull requests

6 participants