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) {