Skip to content

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

@hurali97

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

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