Skip to content

[HOLD for payment 2024-10-04] [$250] Avoid packaging WDYR and whole LODASH in JS bundle #48978

Closed

Description

@hurali97

Problem

In our JS bundle we see that we have unrelated code bundled which is never used. This adds up to the bundle size which eventually leads to the slower loading time for the app. For example, why-did-you-render is present in the JS bundle which is a red flag, since it is installed as a dev dependency. The other case is with Lodash not being able to tree-shake the unused functions.

Apart from this, there are some libraries which are listed in dependencies whereas the right place for them is devDependencies. This doesn't change anything but it's a good practice to keep things where they belong. For example, @kie/act-js, @kie/mock-github, @types/mime-db and others.

To summarise, we have three action items that we will discuss in the solutions:

  • Avoid bundling why-did-you-render in the JS bundle
  • Allow Lodash to tree-shake the unused functions
  • Move the development libraries to devDependencies
  • And a little bonus at the end

Solution

Here's the bundle size as of the baseline ~19.18mb

Screenshot 2024-09-09 at 3 32 06 PM

Avoid bundling why-did-you-render in the JS bundle:

I couldn't figure out the root cause of it. When I comment out the whole wdyr.ts, then we don't have it bundled. I thought it
might be due to the import type {} from 'wdyr but it's not the case either. For some reasons, useWDYR flag is not respected.

Now, One of the solution we can apply is to ignore why-did-you-render if .env file is either of prod or staging. Below is how we can do this in webpack.common:

Diff
diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index 33fd9131eca..2b2da879b88 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -128,6 +128,13 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
             resourceRegExp: /^\.\/locale$/,
             contextRegExp: /moment$/,
         }),
+        ...(file === '.env.production' || file === '.env.staging'
+            ? [
+                  new IgnorePlugin({
+                      resourceRegExp: /@welldone-software\/why-did-you-render/,
+                  }),
+              ]
+            : []),
         ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []),
         new DefinePlugin({
             ...(platform === 'desktop' ? {} : {process: {env: {}}}),
This brings our bundle size to ~19.16mb giving us ~20kb reduction

Expensify Performance Board Screenshot


Allow Lodash to tree-shake the unused functions

We have lots of imports like import {} from 'lodash or import _ from 'lodash. These all doesn't allow web pack to tree shake Lodash. This brings in a ~110kb increase to the bundle size.

See Lodash size

Screenshot 2024-09-09 at 2 57 57 PM

One solution can be to refactor all these imports to named imports like import isEqual from 'lodash/isEqual'.

Otherwise, a better and simple solution with less changes is to use lodash-es and resolve lodash: lodash-es in from web pack. This avoids the need of a massive refactor. Since esmodules have better tree-shake support with web pack the syntax import {} from 'lodash works in terms of tree-shaking. However, we still have to refactor some imports like import _ from 'lodash' to the named ones.

Opting for the second solution brings us ~70kb reduction and the final bundle size is ~19.09mb

Screenshot 2024-09-09 at 3 17 29 PM

Irrespective of the gains, we see that the size of lodash and lodash-es is ~110kb. The reason for this is we have the whole imports or de-structured imports in expensify-common. Once we change those to named imports, everything falls into the place and lodash-es now takes ~50kb, which reduces the bundle size to ~19.04mb.

See bundle size

Screenshot 2024-09-10 at 4 56 50 PM


Move the development libraries to devDependencies

This is self explanatory. We just have to move the libraries which are listed in dependencies but their right place is devDependencies. This doesn't bring any improvement in the bundle size but it's good to place things where they belong.

See diff
diff --git a/package.json b/package.json
index 2f428fa0a14..d2f2f86e465 100644
--- a/package.json
+++ b/package.json
@@ -67,8 +67,6 @@
     "web:prod": "http-server ./dist --cors"
   },
   "dependencies": {
-    "@babel/plugin-proposal-private-methods": "^7.18.6",
-    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
     "@dotlottie/react-player": "^1.6.3",
     "@expensify/react-native-live-markdown": "0.1.120",
     "@expo/metro-runtime": "~3.1.1",
@@ -77,14 +75,10 @@
     "@formatjs/intl-locale": "^4.0.0",
     "@formatjs/intl-numberformat": "^8.10.3",
     "@formatjs/intl-pluralrules": "^5.2.14",
-    "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo",
-    "@fullstory/babel-plugin-react-native": "^1.2.1",
     "@fullstory/browser": "^2.0.3",
     "@fullstory/react-native": "^1.4.2",
     "@gorhom/portal": "^1.0.14",
     "@invertase/react-native-apple-authentication": "^2.2.2",
-    "@kie/act-js": "^2.6.2",
-    "@kie/mock-github": "2.0.1",
     "@onfido/react-native-sdk": "10.6.0",
     "@react-native-camera-roll/camera-roll": "7.4.0",
     "@react-native-clipboard/clipboard": "^1.13.2",
@@ -102,9 +96,7 @@
     "@react-ng/bounds-observer": "^0.2.1",
     "@rnmapbox/maps": "10.1.26",
     "@shopify/flash-list": "1.7.1",
-    "@types/mime-db": "^1.43.5",
     "@ua/react-native-airship": "19.2.1",
-    "@vue/preload-webpack-plugin": "^2.0.0",
     "awesome-phonenumber": "^5.4.0",
     "babel-polyfill": "^6.26.0",
     "canvas-size": "^1.2.6",
@@ -122,8 +114,6 @@
     "focus-trap-react": "^10.2.3",
     "htmlparser2": "^7.2.0",
     "idb-keyval": "^6.2.1",
-    "jest-expo": "51.0.3",
-    "jest-when": "^3.5.2",
     "lodash": "4.17.21",
     "lottie-react-native": "6.5.1",
     "mapbox-gl": "^2.15.0",
@@ -133,7 +123,6 @@
     "react": "18.3.1",
     "react-beautiful-dnd": "^13.1.1",
     "react-collapse": "^5.1.0",
-    "react-compiler-runtime": "file:./lib/react-compiler-runtime",
     "react-content-loader": "^7.0.0",
     "react-dom": "18.3.1",
     "react-error-boundary": "^4.0.11",
@@ -189,9 +178,7 @@
     "react-plaid-link": "3.3.2",
     "react-web-config": "^1.0.0",
     "react-webcam": "^7.1.1",
-    "react-window": "^1.8.9",
-    "semver": "^7.5.2",
-    "xlsx": "file:vendor/xlsx-0.20.3.tgz"
+    "react-window": "^1.8.9"
   },
   "devDependencies": {
     "@actions/core": "1.10.0",
@@ -200,6 +187,8 @@
     "@babel/parser": "^7.22.16",
     "@babel/plugin-proposal-class-properties": "^7.12.1",
     "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
+    "@babel/plugin-proposal-private-methods": "^7.18.6",
+    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
     "@babel/preset-env": "^7.20.0",
     "@babel/preset-flow": "^7.12.13",
     "@babel/preset-react": "^7.10.4",
@@ -210,7 +199,11 @@
     "@callstack/reassure-compare": "^1.0.0-rc.4",
     "@dword-design/eslint-plugin-import-alias": "^5.0.0",
     "@electron/notarize": "^2.1.0",
+    "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo",
+    "@fullstory/babel-plugin-react-native": "^1.2.1",
     "@jest/globals": "^29.5.0",
+    "@kie/act-js": "^2.6.2",
+    "@kie/mock-github": "2.0.1",
     "@ngneat/falso": "^7.1.1",
     "@octokit/core": "4.0.4",
     "@octokit/plugin-paginate-rest": "3.1.0",
@@ -242,6 +235,7 @@
     "@types/js-yaml": "^4.0.5",
     "@types/lodash": "^4.14.195",
     "@types/mapbox-gl": "^2.7.13",
+    "@types/mime-db": "^1.43.5",
     "@types/node": "^20.11.5",
     "@types/pusher-js": "^5.1.0",
     "@types/react": "^18.2.6",
@@ -258,6 +252,7 @@
     "@typescript-eslint/eslint-plugin": "^7.13.1",
     "@typescript-eslint/parser": "^7.13.1",
     "@vercel/ncc": "0.38.1",
+    "@vue/preload-webpack-plugin": "^2.0.0",
     "@welldone-software/why-did-you-render": "7.0.1",
     "ajv-cli": "^5.0.0",
     "babel-jest": "29.4.1",
@@ -293,7 +288,9 @@
     "jest-circus": "29.4.1",
     "jest-cli": "29.4.1",
     "jest-environment-jsdom": "^29.4.1",
+    "jest-expo": "51.0.3",
     "jest-transformer-svg": "^2.0.1",
+    "jest-when": "^3.5.2",
     "link": "^2.1.1",
     "memfs": "^4.6.0",
     "onchange": "^7.1.0",
@@ -304,10 +301,12 @@
     "prettier": "^2.8.8",
     "pusher-js-mock": "^0.3.3",
     "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725",
+    "react-compiler-runtime": "file:./lib/react-compiler-runtime",
     "react-is": "^18.3.1",
     "react-native-clean-project": "^4.0.0-alpha4.0",
     "react-test-renderer": "18.3.1",
     "reassure": "^1.0.0-rc.4",
+    "semver": "^7.5.2",
     "setimmediate": "^1.0.5",
     "shellcheck": "^1.1.0",
     "source-map": "^0.7.4",
@@ -325,6 +324,7 @@
     "webpack-cli": "^5.0.4",
     "webpack-dev-server": "^5.0.4",
     "webpack-merge": "^5.8.0",
+    "xlsx": "file:vendor/xlsx-0.20.3.tgz",
     "yaml": "^2.2.1"
   },
   "overrides": {

Bonus

This is something I found out while figuring out different solutions for tree-shaking Lodash. There's a plugin called babel-plugin-lodash which only picks the used code from lodash but it didn't work. However, while trying it out we have to set:

['@babel/preset-env', {targets: {node: 20}}]

Now, the lodash-plugin takes no effect on our bundle BUT this target setting reduces the bundle size to 18.4mb. This happens because we are now not targeting the oldest possible browser. This is also recommended in babel docs to specify the target for having reduced bundle size.

Screenshot 2024-09-11 at 4 47 54 PM

From my testing, setting to target: node, 20 doesn't break anything. We can do QA and see if there's something wrong and adjust the node version accordingly.

Final bundle size 18.4mb

Screenshot 2024-09-11 at 4 50 47 PM

Issue OwnerCurrent Issue Owner: @
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021843735541723310379
  • Upwork Job ID: 1843735541723310379
  • Last Price Increase: 2024-10-08
Issue OwnerCurrent Issue Owner: @sobitneupane

Activity

  1. melvin-bot commented on Sep 16, 2024

    @melvin-bot

    Current assignee @mountiny is eligible for the AutoAssignerNewDotQuality assigner, not assigning anyone new.

  2. szymonrybczak commented on Sep 17, 2024

    @szymonrybczak
    Contributor

    Hey I’m Szymon from Callstack - I will take this over from Hur!

  3. melvin-bot commented on Sep 17, 2024

    @melvin-bot

    📣 @szymonrybczak! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  4. melvin-bot commented on Sep 20, 2024

    @melvin-bot

    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

    If a regression has occurred and you are the assigned CM follow the instructions here.

    If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

  5. 22 remaining items

  6. removed
    ExternalAdded to denote the issue can be worked on by a contributor
    Help WantedApply this label when an issue is open to proposals by contributors
    on Oct 8, 2024
  7. garrettmknight commented on Oct 8, 2024

    @garrettmknight
    Contributor

    @ishpaul777 offer's out to you.

  8. garrettmknight commented on Oct 8, 2024

    @garrettmknight
    Contributor

    Paid!

  9. sobitneupane commented on Oct 21, 2024

    @sobitneupane
    Contributor

    I reviewed #49059 PR associated with this issue. I will need payment summary to request payment.

    cc: @mountiny @garrettmknight

  10. garrettmknight commented on Oct 22, 2024

    @garrettmknight
    Contributor

    @mountiny can you add another payment summary for Sobit? I can't since I'm his approver in ND.

  11. mountiny commented on Oct 23, 2024

    @mountiny
    Contributor

    Sorry for missing that @sobitneupane

    Given the testing and review of this PR, we should pay $250 to @sobitneupane as well

  12. garrettmknight commented on Oct 28, 2024

    @garrettmknight
    Contributor

    @sobitneupane request when you're ready!

  13. melvin-bot commented on Oct 28, 2024

    @melvin-bot

    @garrettmknight @mountiny Be sure to fill out the Contact List!

  14. JmillsExpensify commented on Dec 12, 2024

    @JmillsExpensify
    Contributor

    Payment summary:

  15. garrettmknight commented on Dec 12, 2024

    @garrettmknight
    Contributor

    $250 approved for @sobitneupane

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

Metadata

Metadata

Labels

AutoAssignerNewDotQualityUsed to assign quality issues to engineersAwaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2NewFeatureSomething to build that is a new item.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions