From c50efafa93e567fa2990ac8d2dbcb84d97174ba4 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 27 Aug 2024 07:23:45 -0700 Subject: [PATCH] Resolver perf: Weakly cache normalisation of `exports` field (4/n) (#1334) Summary: Pull Request resolved: https://github.com/facebook/metro/pull/1334 Typically, a given package is required multiple times within a project, but currently we normalise the same `exports` JSON for every dependency. In RNTester, only three of its dependencies have non-primitive `exports` fields: ``` /node_modules/babel/runtime /node_modules/react /packages/react-native-test-library ``` But these are referenced 760 times, meaning 760 calls to the internal `normalizeExportsField`. This diff uses the property of Metro's upstream `ModuleCache._packageCache`, which avoids reading/parsing `package.json` unless it has changed. This makes `exports` stable, and (when it is non-primitive), weakly referenceable. By caching these values we reduce time spent in `normalizeExportsField` by ~10x and resolution time overall by 10% for RNTester, though the impact is likely to be larger for larger projects. By using a `WeakMap`, the cache can be GCed as corresponding packages are deleted from `ModuleCache`. ``` - **[Performance]**: Cache normalisation of `exports` fields for improved resolution performance. ``` Differential Revision: D61841586 --- .../src/PackageExportsResolve.js | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/metro-resolver/src/PackageExportsResolve.js b/packages/metro-resolver/src/PackageExportsResolve.js index 8bd2c463da..fd9f177b63 100644 --- a/packages/metro-resolver/src/PackageExportsResolve.js +++ b/packages/metro-resolver/src/PackageExportsResolve.js @@ -141,6 +141,25 @@ function getExportsSubpath(packageSubpath: string): string { return packageSubpath === '' ? '.' : './' + toPosixPath(packageSubpath); } +/** + * Maintain a WeakMap cache of the results of normalizedExportsField. + * Particularly in a large project, many source files depend on the same + * packages (eg @babel/runtime), and this avoids normalising the same JSON + * many times. Note that ExportsField is immutable, and the upstream package + * cache gives us a stable reference. + * + * The case where ExportsField is a string (not weakly referencable) has to be + * excluded, but those are very cheap to process anyway. + * + * (Ultimately this should be coupled more closely to the package cache, so that + * we can clean up immediately rather than on GC.) + */ +type ExcludeString = T extends string ? empty : T; +const _normalizedExportsFields: WeakMap< + ExcludeString, + ExportMap, +> = new WeakMap(); + /** * Normalise an "exports"-like field by parsing string shorthand and conditions * shorthand at root, and flattening any legacy Node.js <13.7 array values. @@ -153,6 +172,15 @@ function normalizeExportsField( ): ExportMap { let rootValue; + if (typeof exportsField === 'string') { + return {'.': exportsField}; + } + + const cachedValue = _normalizedExportsFields.get(exportsField); + if (cachedValue) { + return cachedValue; + } + if (Array.isArray(exportsField)) { // If an array of strings, use first value with valid specifier (root shorthand) if (exportsField.every(value => typeof value === 'string')) { @@ -173,7 +201,9 @@ function normalizeExportsField( } if (typeof rootValue === 'string') { - return {'.': rootValue}; + const result = {'.': rootValue}; + _normalizedExportsFields.set(exportsField, result); + return result; } const firstLevelKeys = Object.keys(rootValue); @@ -182,7 +212,9 @@ function normalizeExportsField( ); if (subpathKeys.length === firstLevelKeys.length) { - return flattenLegacySubpathValues(rootValue, createConfigError); + const result = flattenLegacySubpathValues(rootValue, createConfigError); + _normalizedExportsFields.set(exportsField, result); + return result; } if (subpathKeys.length !== 0) { @@ -192,7 +224,11 @@ function normalizeExportsField( ); } - return {'.': flattenLegacySubpathValues(rootValue, createConfigError)}; + const result = { + '.': flattenLegacySubpathValues(rootValue, createConfigError), + }; + _normalizedExportsFields.set(exportsField, result); + return result; } /**