storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
83.94k stars 9.21k forks source link

[Tracking]: Replace Lodash Usage with Native Functionality or Alternatives #28611

Open valentinpalkovic opened 1 month ago

valentinpalkovic commented 1 month ago

Overview

In our efforts to optimize our monorepo's dependencies and improve bundle sizes, we are looking to replace our usage of Lodash with native JavaScript functionality or alternative libraries where applicable. This initiative is inspired by the recommendations found in You Don't Need Lodash/Underscore, which outlines many cases where modern JavaScript provides solutions that previously required utility libraries like Lodash.

Specific Lodash Functions in Use

From an initial audit, the following Lodash functions are currently in use across various packages in our monorepo:

Acceptance Criteria

To consider this work complete, the following criteria must be met:

  1. No Direct Lodash Imports: Direct imports from Lodash (e.g., import merge from 'lodash/merge') should be replaced across the monorepo.
  2. Functionality Preservation: Replacements must maintain existing functionality. Unit tests should pass without modifications to the tests themselves (except of changing the way how lodash functionalities might get mocked), indicating that behavior is preserved.
  3. Bundle Size Reduction: The overall bundle size of our packages should decrease as a result of removing Lodash, measured by our current bundle analysis tools.
  4. Performance Considerations: Any native or alternative solutions should not introduce significant performance regressions.
  5. Code Review: Changes should be reviewed for readability and maintainability. While functional replacements are the goal, we should avoid overly complex or cryptic solutions in favor of clarity.

Potential Challenges

Request for Contributions

We are seeking contributions from the open source community to tackle this initiative. Contributions can range from identifying and documenting potential replacements, to implementing changes in specific packages. Please comment on this issue if you're able to take on a part of this work, specifying which packages or Lodash functions you're focusing on.

Thank you for your contributions to improving our monorepo's efficiency and maintainability!

greptile-apps[bot] commented 1 month ago

Disclaimer This information might be inaccurate, due to it being generated automatically

Implementation Steps

  1. Replace lodash/camelCase:

    • Use a custom function or a library like change-case.
    • Example: import { camelCase } from 'change-case';
  2. Replace lodash/cloneDeep:

    • Use structuredClone or a custom deep clone function.
    • Example: const clone = structuredClone(obj);
  3. Replace lodash/countBy:

    • Use Array.prototype.reduce to count occurrences.
    • Example: const countBy = (arr, fn) => arr.reduce((acc, val) => { const key = fn(val); acc[key] = (acc[key] || 0) + 1; return acc; }, {});
  4. Replace lodash/debounce:

    • Use a custom debounce function.
    • Example: const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; };
  5. Replace lodash/isEqual:

    • Use a deep equality check function.
    • Example: const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
  6. Replace lodash/isPlainObject:

    • Use typeof and Object.prototype.toString.
    • Example: const isPlainObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]';
  7. Replace lodash/kebabCase:

    • Use a custom function or a library like change-case.
    • Example: import { paramCase } from 'change-case';
  8. Replace lodash/mapKeys:

    • Use Object.entries and Array.prototype.reduce.
    • Example: const mapKeys = (obj, fn) => Object.entries(obj).reduce((acc, [key, value]) => { acc[fn(key)] = value; return acc; }, {});
  9. Replace lodash/mapValues:

    • Use Object.entries and Array.prototype.reduce.
    • Example: const mapValues = (obj, fn) => Object.entries(obj).reduce((acc, [key, value]) => { acc[key] = fn(value); return acc; }, {});
  10. Replace lodash/merge and lodash/mergeWith:

    • Use Object.assign or a custom deep merge function.
    • Example: const merge = (target, ...sources) => sources.reduce((acc, src) => { Object.keys(src).forEach(key => { if (typeof src[key] === 'object' && src[key] !== null) { if (!acc[key]) acc[key] = {}; merge(acc[key], src[key]); } else { acc[key] = src[key]; } }); return acc; }, target);
  11. Replace lodash/pick and lodash/pickBy:

    • Use Object.keys and Array.prototype.reduce.
    • Example: const pick = (obj, keys) => keys.reduce((acc, key) => { if (obj.hasOwnProperty(key)) acc[key] = obj[key]; return acc; }, {});
  12. Replace lodash/startCase:

    • Use a custom function or a library like change-case.
    • Example: import { startCase } from 'change-case';
  13. Replace lodash/throttle:

    • Use a custom throttle function.
    • Example: const throttle = (func, limit) => { let inThrottle; return (...args) => { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; };
  14. Replace lodash/uniq:

    • Use Array.prototype.filter and Set.
    • Example: const uniq = (arr) => [...new Set(arr)];
  15. Replace lodash/upperFirst:

    • Use a custom function.
    • Example: const upperFirst = (str) => str.charAt(0).toUpperCase() + str.slice(1);

Code References

Testing

References

/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts /code/lib/cli/src/automigrate/fixes/prompt-remove-react.ts /code/lib/cli/src/automigrate/fixes/prompt-remove-react.test.ts /code/lib/cli/src/automigrate/fixes/webpack5-compiler-setup.test.ts /code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts /code/lib/cli/src/automigrate/fixes/mdx-to-csf.test.ts /code/lib/cli/src/automigrate/fixes/storyshots-migration.test.ts /code/lib/cli/src/automigrate/fixes/remove-global-client-apis.test.ts /code/lib/cli/src/automigrate/fixes/new-frameworks.test.ts /code/lib/cli/src/automigrate/fixes/sb-scripts.test.ts /code/lib/cli/src/automigrate /code/lib/cli/src/automigrate/helpers /code/core/src/builder-manager/utils/framework.ts /code/core/src/builder-manager/utils/framework.test.ts /code/core/src/telemetry/get-monorepo-type.test.ts /code/lib/cli/src/automigrate/index.test.ts

#### About Greptile This response provides a starting point for your research, not a precise solution. Help us improve! Please leave a ๐Ÿ‘ if this is helpful and ๐Ÿ‘Ž if it is irrelevant. [Ask Greptile](https://app.greptile.com/chat/github/storybookjs/storybook/next) ยท [Edit Issue Bot Settings](https://app.greptile.com/apps/github)
stolbikova commented 1 month ago

Hello, I am new here so I might ask some silly questions.

Which bundle exactly is it preferable to analyze? manager-bundle, common-manager-bundle or globals-runtime? Is it possible to contribute gradually, by refactoring one function in one PR?

Thank you.

xeho91 commented 1 month ago

Can deepmerge-ts be a potential candidate for replacing merge?

It possibly can replace mergeWith with deepmergeCustom as well.

tanel-terras commented 1 month ago

es-toolkit could be considered as an alternative to lodash - https://github.com/toss/es-toolkit

valentinpalkovic commented 1 month ago

Hello, I am new here so I might ask some silly questions.

Which bundle exactly is it preferable to analyze? manager-bundle, common-manager-bundle or globals-runtime? Is it possible to contribute gradually, by refactoring one function in one PR?

Thank you.

The biggest impact would be to get rid of lodash in @storybook/core. Due to how the sub packages of @storybook/core are bundled, lodash occurs several times in the built output.

Also feel free to contribute one PR at a time to remove/replace single occurrences of lodash calls.

As soon as you open a PR, you will see some benchmark stats about bundle size.

valentinpalkovic commented 1 month ago

Can deepmerge-ts be a potential candidate for replacing merge?

It possibly can replace mergeWith with deepmergeCustom as well.

Would replacing merge from lodash with deepmerge-ts have a positive impact on bundle size? We have some benchmarking in place as soon as you create a PR. The manager's and preview's bundle size will be reported. So just open a PR to figure out its impact.

valentinpalkovic commented 1 month ago

es-toolkit could be considered as an alternative to lodash - https://github.com/toss/es-toolkit

Seems to be pretty interesting! @xeho91, @tanel-terras, @stolbikova do you have any experience or opinion about es-toolkit?

valentinpalkovic commented 1 month ago

I analyzed es-toolkit, and it seems it doesn't cover the following lodash functions (yet):

I think to split the work so that multiple contributors could contribute, we could do the following:

Workstream: Introduce es-toolkit/compat

Workstream: Find alternatives for the functions, which are not replaceable

xeho91 commented 1 month ago

Can deepmerge-ts be a potential candidate for replacing merge? It possibly can replace mergeWith with deepmergeCustom as well.

Would replacing merge from lodash with deepmerge-ts have a positive impact on bundle size? We have some benchmarking in place as soon as you create a PR. The manager's and preview's bundle size will be reported. So just open a PR to figure out its impact.

Are there any set boundaries for maximum positive impact on the bundle size?

Is very minimal package, 5,8kB and tree-shakeable: https://bundlephobia.com/package/deepmerge-ts@7.0.3

And also, not 'polluted', 0 dependencies: https://npmgraph.js.org/?q=deepmerge-ts

PR for preview: https://github.com/storybookjs/storybook/pull/28663

xeho91 commented 1 month ago

es-toolkit could be considered as an alternative to lodash - https://github.com/toss/es-toolkit

Seems to be pretty interesting! @xeho91, @tanel-terras, @stolbikova do you have any experience or opinion about es-toolkit?

Honestly, no strong opinion.

I definitely would favour this more over lodash, because how well maintained, and even typed this collection is. Benchmarks included as well, awesome to see how much possible positive impact we could get from replacing.

You already observed that it cannot replace all of the existing lodash snippets, but is a good start, and definitely worth a try/effort. ๐Ÿ‘

raon0211 commented 4 weeks ago

Hello! I'm the maintainer of es-toolkit.

Just a quick update to let you know that we've added several new functions to es-toolkit, including:

Please note that there are some differences between es-toolkit and lodash, designed to optimize bundle size and runtime performance. For example:

In the meantime, if you need full compatibility with lodash, you might want to use es-toolkit/compat, which fully supports and tests all behaviors from lodash.

valentinpalkovic commented 3 weeks ago

Hi @raon0211! That's amazing. Thank you for letting us know.