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

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

Closed
17 tasks
Tracked by #29038
valentinpalkovic opened this issue Jul 15, 2024 · 12 comments · Fixed by #28981
Closed
17 tasks
Tracked by #29038

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

valentinpalkovic opened this issue Jul 15, 2024 · 12 comments · Fixed by #28981

Comments

@valentinpalkovic
Copy link
Contributor

valentinpalkovic commented Jul 15, 2024

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:

  • lodash/camelCase
  • lodash/cloneDeep
  • lodash/countBy
  • lodash/debounce
  • lodash/isEqual
  • lodash/isPlainObject
  • lodash/kebabCase
  • lodash/mapKeys
  • lodash/mapValues
  • lodash/merge
  • lodash/mergeWith
  • lodash/pick
  • lodash/pickBy
  • lodash/startCase
  • lodash/throttle
  • lodash/uniq
  • lodash/upperFirst

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

  • Complexity of merge and mergeWith: These functions are used extensively for deep object merging. Identifying a suitable replacement that handles all edge cases might be challenging.
  • Throttling: The lodash/throttle function is used in performance-sensitive areas. Ensuring that the replacement maintains the same throttling behavior and performance characteristics is crucial.

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!

Copy link
Contributor

greptile-apps bot commented Jul 15, 2024

** 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

  • Refer to /code/lib/cli/src/automigrate/helpers for utility functions that might be useful.
  • Update the relevant files in /code/lib/cli/src/automigrate/fixes to replace Lodash functions.

Testing

  • Ensure all unit tests pass without modifications to the tests themselves, except for changing the way Lodash functionalities might get mocked.
  • Measure bundle size reduction using current bundle analysis tools.

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 · Edit Issue Bot Settings

@valentinpalkovic valentinpalkovic changed the title [Feature Request]: Replace Lodash Usage with Native Functionality or Alternatives [Tracking]: Replace Lodash Usage with Native Functionality or Alternatives Jul 15, 2024
@stolbikova
Copy link

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
Copy link

xeho91 commented Jul 16, 2024

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

It possibly can replace mergeWith with deepmergeCustom as well.

@tanel-terras
Copy link

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

@valentinpalkovic
Copy link
Contributor Author

valentinpalkovic commented Jul 18, 2024

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
Copy link
Contributor Author

valentinpalkovic commented Jul 18, 2024

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
Copy link
Contributor Author

valentinpalkovic commented Jul 18, 2024

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
Copy link
Contributor Author

valentinpalkovic commented Jul 19, 2024

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

  • lodash/cloneDeep (PR available)
  • lodash/isPlainObject
  • lodash/mapKeys
  • lodash/mapValues
  • lodash/merge
  • lodash/mergeWith

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

Workstream: Introduce es-toolkit/compat

  • Let's introduce https://es-toolkit.slash.page/compatibility.html to have maximum compatibility with lodash, as es-toolkit/compat is a compatibility layer that bridges the gap between the two libraries.
  • Let's replace all replaceable lodash functions by es-toolkit
  • In the last step lets try to migrate away from the compat layer and use es-toolkit "purely"

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

  • The aforementioned functions like lodash/cloneDeep need alternatives. Ideally, we should analyze the code, understand its context, and figure out how we can best replace its usage.
  • Another approach would be to contribute to es-toolkit directly. My feeling says, though, that this might take a bit of time.

@xeho91
Copy link

xeho91 commented Jul 20, 2024

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/[email protected]

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

PR for preview: #28663

@xeho91
Copy link

xeho91 commented Jul 20, 2024

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
Copy link
Contributor

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:

  • es-toolkit does not clone values like Object(true) or arguments in a special way.
  • es-toolkit does not check for Symbol.toStringTag.
  • es-toolkit does not support special property syntax for object mapping, such as mapKeys(arr, 'property'). Instead, use mapKeys(arr, x => x.property).
  • es-toolkit does not support merging multiple objects at once with merge(obj1, obj2, obj3) or mergeWith(obj1, obj2, obj3). You can use merge or mergeWith iteratively instead.

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
Copy link
Contributor Author

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

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

Successfully merging a pull request may close this issue.

5 participants