From 16fd63cbbfbe135e10aa4f30873253c064bb1699 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:36:19 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Avoid=20intermediate=20buff?= =?UTF-8?q?er=20allocations=20in=20fs=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💡 What Replaced usages of `fs.readFileSync(path).toString()` and `fs.readFile(path).toString()` with `fs.readFileSync(path, 'utf8')` and `fs.readFile(path, 'utf8')` across various packages (e.g., `@expo/cli`, `@expo/config-plugins`, `expo-modules-autolinking`). 🎯 Why When reading files in Node.js, calling `.toString()` on the returned `Buffer` allocates an intermediate `Buffer` object before converting it to a V8 string, which adds unnecessary memory allocation and garbage collection overhead. By explicitly providing the `'utf8'` encoding flag to the `fs` methods, Node.js bypasses the intermediate `Buffer` and streams the file contents directly into a V8 string. 📊 Impact Reduces memory allocations and GC overhead when reading text-based source files and configurations during build and development processes. 🔬 Measurement Measure heap memory allocation profiles when performing operations that read many files, such as resolving dependencies in `expo-modules-autolinking` or reading configs in `@expo/cli`. Co-authored-by: vishnu-madhavan-git <237662584+vishnu-madhavan-git@users.noreply.github.com> --- packages/@expo/cli/src/events/stream.ts | 2 +- .../@expo/cli/src/export/exportDomComponents.ts | 12 +++++++++--- .../src/start/project/__tests__/devices-test.ts | 2 +- .../@expo/cli/src/utils/mergeGitIgnorePaths.ts | 4 ++-- packages/@expo/cli/src/utils/plist.ts | 2 +- .../@expo/config-plugins/src/android/Package.ts | 4 ++-- .../src/android/__tests__/Locales-test.ts | 17 ++++++++++------- .../__tests__/renamePackageOnDisk-test.ts | 12 ++++++------ .../withAndroidSplashLegacyMainActivity-test.ts | 2 +- .../plugin/src/common/filesystem.ts | 4 ++-- .../__tests__/androidResolver-test.ts | 4 ++-- .../src/reactNativeConfig/androidResolver.ts | 12 ++++++------ packages/pod-install/src/index.ts | 2 +- 13 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/@expo/cli/src/events/stream.ts b/packages/@expo/cli/src/events/stream.ts index 94464d1065e16f..539a61229afd35 100644 --- a/packages/@expo/cli/src/events/stream.ts +++ b/packages/@expo/cli/src/events/stream.ts @@ -61,7 +61,7 @@ export class LogStream extends EventEmitter implements NodeJS.WritableStream { const outputLength = Buffer.byteLength(this.#output); if (outputLength > written) { - const output = Buffer.from(this.#output).subarray(written).toString(); + const output = Buffer.from(this.#output).subarray(written).toString('utf8'); this.#len -= this.#output.length - output.length; this.#output = output; } else { diff --git a/packages/@expo/cli/src/export/exportDomComponents.ts b/packages/@expo/cli/src/export/exportDomComponents.ts index 91e2dbb6dd1d1a..b00d80dece2cf2 100644 --- a/packages/@expo/cli/src/export/exportDomComponents.ts +++ b/packages/@expo/cli/src/export/exportDomComponents.ts @@ -139,7 +139,10 @@ export function transformDomEntryForMd5Filename({ }): PlatformMetadata['assets'] { const htmlContent = files.get(htmlOutputName); assert(htmlContent); - const htmlMd5 = crypto.createHash('md5').update(htmlContent.contents.toString()).digest('hex'); + const htmlMd5 = crypto + .createHash('md5') + .update(htmlContent.contents.toString('utf8')) + .digest('hex'); const htmlMd5Filename = `${DOM_COMPONENTS_BUNDLE_DIR}/${htmlMd5}.html`; files.set(htmlMd5Filename, htmlContent); files.delete(htmlOutputName); @@ -167,7 +170,10 @@ export function transformNativeBundleForMd5Filename({ }) { const htmlContent = files.get(htmlOutputName); assert(htmlContent); - const htmlMd5 = crypto.createHash('md5').update(htmlContent.contents.toString()).digest('hex'); + const htmlMd5 = crypto + .createHash('md5') + .update(htmlContent.contents.toString('utf8')) + .digest('hex'); const hash = crypto.createHash('md5').update(domComponentReference).digest('hex'); for (const artifact of nativeBundle.artifacts) { if (artifact.type !== 'js') { @@ -188,7 +194,7 @@ export function transformNativeBundleForMd5Filename({ const search = `${hash}.html`; const replace = `${htmlMd5}.html`; assert(search.length === replace.length); - assetEntity.contents = assetEntity.contents.toString().replaceAll(search, replace); + assetEntity.contents = assetEntity.contents.toString('utf8').replaceAll(search, replace); } } } diff --git a/packages/@expo/cli/src/start/project/__tests__/devices-test.ts b/packages/@expo/cli/src/start/project/__tests__/devices-test.ts index 47cca6966ff94d..3eb4feb23905b1 100644 --- a/packages/@expo/cli/src/start/project/__tests__/devices-test.ts +++ b/packages/@expo/cli/src/start/project/__tests__/devices-test.ts @@ -26,7 +26,7 @@ describe('devices info', () => { const file = path.join(projectRoot, '.expo', 'devices.json'); expect(fs.existsSync(file)).toBe(true); - const { devices } = JSON.parse(fs.readFileSync(file, 'utf8').toString()); + const { devices } = JSON.parse(fs.readFileSync(file, 'utf8')); expect(devices.length).toBe(1); expect(devices[0].installationId).toBe('test-device-id'); }); diff --git a/packages/@expo/cli/src/utils/mergeGitIgnorePaths.ts b/packages/@expo/cli/src/utils/mergeGitIgnorePaths.ts index dab487df678413..8fe635d49973c9 100644 --- a/packages/@expo/cli/src/utils/mergeGitIgnorePaths.ts +++ b/packages/@expo/cli/src/utils/mergeGitIgnorePaths.ts @@ -35,8 +35,8 @@ export function mergeGitIgnorePaths( return null; } - const targetGitIgnore = fs.readFileSync(targetGitIgnorePath).toString(); - const sourceGitIgnore = fs.readFileSync(sourceGitIgnorePath).toString(); + const targetGitIgnore = fs.readFileSync(targetGitIgnorePath, 'utf8'); + const sourceGitIgnore = fs.readFileSync(sourceGitIgnorePath, 'utf8'); const merged = mergeGitIgnoreContents(targetGitIgnore, sourceGitIgnore); // Only rewrite the file if it was modified. if (merged.contents) { diff --git a/packages/@expo/cli/src/utils/plist.ts b/packages/@expo/cli/src/utils/plist.ts index b2688aed586014..9e23c53d8ce0ca 100644 --- a/packages/@expo/cli/src/utils/plist.ts +++ b/packages/@expo/cli/src/utils/plist.ts @@ -22,7 +22,7 @@ export async function parsePlistAsync(plistPath: string) { export function parsePlistBuffer(contents: Buffer) { if (contents[0] === CHAR_CHEVRON_OPEN) { - const info = plist.parse(contents.toString()); + const info = plist.parse(contents.toString('utf8')); if (Array.isArray(info)) return info[0]; return info; } else if (contents[0] === CHAR_B_LOWER) { diff --git a/packages/@expo/config-plugins/src/android/Package.ts b/packages/@expo/config-plugins/src/android/Package.ts index 96222ebf4ba553..052004d5ed719e 100644 --- a/packages/@expo/config-plugins/src/android/Package.ts +++ b/packages/@expo/config-plugins/src/android/Package.ts @@ -125,7 +125,7 @@ export async function renameJniOnDiskForType({ filesToUpdate.forEach((filepath: string) => { try { if (fs.lstatSync(filepath).isFile() && ['.h', '.cpp'].includes(path.extname(filepath))) { - let contents = fs.readFileSync(filepath).toString(); + let contents = fs.readFileSync(filepath, 'utf8'); contents = contents.replace( new RegExp(transformJavaClassDescriptor(currentPackageName).replace(/\//g, '\\/'), 'g'), transformJavaClassDescriptor(packageName) @@ -207,7 +207,7 @@ export async function renamePackageOnDiskForType({ filesToUpdate.forEach((filepath: string) => { try { if (fs.lstatSync(filepath).isFile()) { - let contents = fs.readFileSync(filepath).toString(); + let contents = fs.readFileSync(filepath, 'utf8'); if (path.extname(filepath) === '.kt') { contents = replacePackageName(contents, currentPackageName, kotlinSanitizedPackageName); } else { diff --git a/packages/@expo/config-plugins/src/android/__tests__/Locales-test.ts b/packages/@expo/config-plugins/src/android/__tests__/Locales-test.ts index 9f31ef8fc18685..15a8909f64b7fd 100644 --- a/packages/@expo/config-plugins/src/android/__tests__/Locales-test.ts +++ b/packages/@expo/config-plugins/src/android/__tests__/Locales-test.ts @@ -58,7 +58,7 @@ describe('e2e: Android locales', () => { }, }; const mockJSONFile = { - readAsync: (path) => JSON.parse(vol.readFileSync(path).toString()), + readAsync: (path) => JSON.parse(vol.readFileSync(path, 'utf8') as string), }; jest.mock('../../utils/XML', () => mockXML); jest.mock('@expo/json-file', () => mockJSONFile); @@ -84,22 +84,25 @@ describe('e2e: Android locales', () => { { projectRoot } ); - expect(vol.readFileSync('/app/android/app/src/main/res/values-b+es/strings.xml').toString()) - .toMatchInlineSnapshot(` + expect( + vol.readFileSync('/app/android/app/src/main/res/values-b+es/strings.xml', 'utf8') as string + ).toMatchInlineSnapshot(` " "spanish-name" " `); // backwards compatibility - expect(vol.readFileSync('/app/android/app/src/main/res/values-b+en/strings.xml').toString()) - .toMatchInlineSnapshot(` + expect( + vol.readFileSync('/app/android/app/src/main/res/values-b+en/strings.xml', 'utf8') as string + ).toMatchInlineSnapshot(` " "us-name" "us-name" " `); - expect(vol.readFileSync('/app/android/app/src/main/res/values-b+en+US/strings.xml').toString()) - .toMatchInlineSnapshot(` + expect( + vol.readFileSync('/app/android/app/src/main/res/values-b+en+US/strings.xml', 'utf8') as string + ).toMatchInlineSnapshot(` " "us-name" " diff --git a/packages/@expo/config-plugins/src/android/__tests__/renamePackageOnDisk-test.ts b/packages/@expo/config-plugins/src/android/__tests__/renamePackageOnDisk-test.ts index af4d7c87c84aa5..6aa1e4762456fb 100644 --- a/packages/@expo/config-plugins/src/android/__tests__/renamePackageOnDisk-test.ts +++ b/packages/@expo/config-plugins/src/android/__tests__/renamePackageOnDisk-test.ts @@ -44,24 +44,24 @@ public class SomeClass { await renamePackageOnDisk({ android: { package: 'xyz.bront.app' } }, '/myapp'); const mainActivityPath = '/myapp/android/app/src/main/java/xyz/bront/app/MainActivity.java'; expect(fs.existsSync(mainActivityPath)).toBeTruthy(); - expect(fs.readFileSync(mainActivityPath).toString()).toMatch('package xyz.bront.app'); + expect(fs.readFileSync(mainActivityPath, 'utf8')).toMatch('package xyz.bront.app'); const nestedClassPath = '/myapp/android/app/src/main/java/xyz/bront/app/example/SomeClass.java'; expect(fs.existsSync(nestedClassPath)).toBeTruthy(); - expect(fs.readFileSync(nestedClassPath).toString()).toMatch('package xyz.bront.app'); - expect(fs.readFileSync(nestedClassPath).toString()).not.toMatch('com.lololol'); + expect(fs.readFileSync(nestedClassPath, 'utf8')).toMatch('package xyz.bront.app'); + expect(fs.readFileSync(nestedClassPath, 'utf8')).not.toMatch('com.lololol'); const buckPath = '/myapp/android/app/BUCK'; - expect(fs.readFileSync(buckPath).toString()).toMatch('package = "xyz.bront.app"'); - expect(fs.readFileSync(buckPath).toString()).not.toMatch('com.lololol'); + expect(fs.readFileSync(buckPath, 'utf8')).toMatch('package = "xyz.bront.app"'); + expect(fs.readFileSync(buckPath, 'utf8')).not.toMatch('com.lololol'); }); it('does not clobber itself if package has similar parts', async () => { await renamePackageOnDisk({ android: { package: 'com.bront' } }, '/myapp'); const mainActivityPath = '/myapp/android/app/src/main/java/com/bront/MainActivity.java'; expect(fs.existsSync(mainActivityPath)).toBeTruthy(); - expect(fs.readFileSync(mainActivityPath).toString()).toMatch('package com.bront'); + expect(fs.readFileSync(mainActivityPath, 'utf8')).toMatch('package com.bront'); }); }); }); diff --git a/packages/@expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/__tests__/withAndroidSplashLegacyMainActivity-test.ts b/packages/@expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/__tests__/withAndroidSplashLegacyMainActivity-test.ts index edbcea2fa9eb34..9eaaa5e6efcfff 100644 --- a/packages/@expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/__tests__/withAndroidSplashLegacyMainActivity-test.ts +++ b/packages/@expo/prebuild-config/src/plugins/unversioned/expo-splash-screen/__tests__/withAndroidSplashLegacyMainActivity-test.ts @@ -28,7 +28,7 @@ describe(setSplashScreenLegacyMainActivity, () => { }, }; const mainActivity = await AndroidConfig.Paths.getMainActivityAsync('/app'); - let contents = fs.readFileSync(mainActivity.path).toString(); + let contents = fs.readFileSync(mainActivity.path, 'utf8'); contents = await setSplashScreenLegacyMainActivity( exp, { backgroundColor: '#000020', resizeMode: 'native' }, diff --git a/packages/expo-brownfield/plugin/src/common/filesystem.ts b/packages/expo-brownfield/plugin/src/common/filesystem.ts index dfbcceaeb05b2d..c7809c6db83237 100644 --- a/packages/expo-brownfield/plugin/src/common/filesystem.ts +++ b/packages/expo-brownfield/plugin/src/common/filesystem.ts @@ -26,7 +26,7 @@ const maybeReadOverwrittenTemplate = (template: string, platform?: PlatformStrin try { accessSync(path.join(process.cwd(), '.brownfield-templates')); if (existsSync(path.join(process.cwd(), '.brownfield-templates', template))) { - return readFileSync(path.join(process.cwd(), '.brownfield-templates', template)).toString(); + return readFileSync(path.join(process.cwd(), '.brownfield-templates', template), 'utf8'); } if (existsSync(path.join(process.cwd(), '.brownfield-templates', platform ?? '.', template))) { @@ -55,7 +55,7 @@ const readTemplate = (template: string, platform?: PlatformString): string => { throw new Error(`Template ${template} doesn't exist at ${templatePath}`); } - return readFileSync(templatePath).toString(); + return readFileSync(templatePath, 'utf8'); }; const createFileFromTemplateInternal = ( diff --git a/packages/expo-modules-autolinking/src/reactNativeConfig/__tests__/androidResolver-test.ts b/packages/expo-modules-autolinking/src/reactNativeConfig/__tests__/androidResolver-test.ts index 1fdf240321c211..f9f91aa3f4a332 100644 --- a/packages/expo-modules-autolinking/src/reactNativeConfig/__tests__/androidResolver-test.ts +++ b/packages/expo-modules-autolinking/src/reactNativeConfig/__tests__/androidResolver-test.ts @@ -440,7 +440,7 @@ class CustomReactPackage : TurboReactPackage() { }`, }, ])('should handle $description', ({ content }) => { - expect(matchNativePackageClassName(path, Buffer.from(content))).toBe('CustomReactPackage'); + expect(matchNativePackageClassName(path, content)).toBe('CustomReactPackage'); }); // these are not as exhaustive as they could be, but cover main cases @@ -462,7 +462,7 @@ public class CustomReactPackage extends SomeOtherPackage { content: '', }, ])('should return null for $description', ({ content }) => { - expect(matchNativePackageClassName(path, Buffer.from(content))).toBeNull(); + expect(matchNativePackageClassName(path, content)).toBeNull(); }); }); }); diff --git a/packages/expo-modules-autolinking/src/reactNativeConfig/androidResolver.ts b/packages/expo-modules-autolinking/src/reactNativeConfig/androidResolver.ts index f164936cac009b..f54a6e2da89ab0 100644 --- a/packages/expo-modules-autolinking/src/reactNativeConfig/androidResolver.ts +++ b/packages/expo-modules-autolinking/src/reactNativeConfig/androidResolver.ts @@ -143,7 +143,7 @@ export async function parseNativePackageClassNameAsync( for await (const entry of scanFilesRecursively(androidDir, undefined, true)) { if (entry.name.endsWith('Package.java') || entry.name.endsWith('Package.kt')) { try { - const contents = await fs.readFile(entry.path); + const contents = await fs.readFile(entry.path, 'utf8'); const matched = matchNativePackageClassName(entry.path, contents); if (matched) { return matched; @@ -162,7 +162,7 @@ export async function parseNativePackageClassNameAsync( // Search all **/*.{java,kt} files for await (const entry of scanFilesRecursively(androidDir, undefined, true)) { if (entry.name.endsWith('.java') || entry.name.endsWith('.kt')) { - const contents = await fs.readFile(entry.path); + const contents = await fs.readFile(entry.path, 'utf8'); const matched = matchNativePackageClassName(entry.path, contents); if (matched) { return matched; @@ -174,15 +174,15 @@ export async function parseNativePackageClassNameAsync( let lazyReactPackageRegex: RegExp | null = null; let lazyTurboReactPackageRegex: RegExp | null = null; -export function matchNativePackageClassName(_filePath: string, contents: Buffer): string | null { - const fileContents = contents.toString(); +export function matchNativePackageClassName(_filePath: string, contents: string): string | null { + // [0] Match ReactPackage if (!lazyReactPackageRegex) { lazyReactPackageRegex = /class\s+(\w+[^(\s]*)[\s\w():]*(\s+implements\s+|:)[\s\w():,]*[^{]*ReactPackage/; } - const matchReactPackage = fileContents.match(lazyReactPackageRegex); + const matchReactPackage = contents.match(lazyReactPackageRegex); if (matchReactPackage) { return matchReactPackage[1]; } @@ -192,7 +192,7 @@ export function matchNativePackageClassName(_filePath: string, contents: Buffer) lazyTurboReactPackageRegex = /class\s+(\w+[^(\s]*)[\s\w():]*(\s+extends\s+|:)[\s\w():,]*[^{]*(Base|Turbo)ReactPackage/; } - const matchTurboReactPackage = fileContents.match(lazyTurboReactPackageRegex); + const matchTurboReactPackage = contents.match(lazyTurboReactPackageRegex); if (matchTurboReactPackage) { return matchTurboReactPackage[1]; } diff --git a/packages/pod-install/src/index.ts b/packages/pod-install/src/index.ts index 15184c3f113372..56650359e3df6b 100644 --- a/packages/pod-install/src/index.ts +++ b/packages/pod-install/src/index.ts @@ -41,7 +41,7 @@ async function runAsync(maybeProjectDirectory?: string): Promise { process.exit(1); } - const jsonData = JSON.parse(readFileSync(packageJsonPath).toString()); + const jsonData = JSON.parse(readFileSync(packageJsonPath, 'utf8')); const hasExpoPackage = jsonData.dependencies?.hasOwnProperty('expo'); if (hasExpoPackage) {