perf: prefer async fs calls (#11918)

Synchronous file system operations such as `readFileSync` block the
event loop, whereas the asynchronous equivalents (like await
`fs.promises.readFile`) do not. This PR replaces certain synchronous fs
calls with their asynchronous counterparts in contexts where async
operations are already in use, improving performance by avoiding event
loop blocking.

Most of the synchronous calls were in our file upload code. Converting
them to async should theoretically free up the event loop and allow
more, other requests to run in parallel without delay
This commit is contained in:
Alessio Gravili
2025-03-29 10:58:54 -06:00
committed by GitHub
parent 70b9cab393
commit d1c0989da7
11 changed files with 30 additions and 34 deletions

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import fs from 'fs'
import fs from 'fs/promises'
import process from 'node:process'
import type { PayloadComponent, SanitizedConfig } from '../../config/types.js'
@@ -147,7 +147,7 @@ ${mapKeys.join(',\n')}
if (!force) {
// Read current import map and check in the IMPORTS if there are any new imports. If not, don't write the file.
const currentImportMap = await fs.promises.readFile(importMapFilePath, 'utf-8')
const currentImportMap = await fs.readFile(importMapFilePath, 'utf-8')
if (currentImportMap?.trim() === importMapOutputFile?.trim()) {
if (log) {
@@ -161,5 +161,5 @@ ${mapKeys.join(',\n')}
console.log('Writing import map to', importMapFilePath)
}
await fs.promises.writeFile(importMapFilePath, importMapOutputFile)
await fs.writeFile(importMapFilePath, importMapOutputFile)
}

View File

@@ -1,7 +1,7 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations'
import fs from 'fs'
import fs from 'fs/promises'
import { compile } from 'json-schema-to-typescript'
import type { SanitizedConfig } from '../config/types.js'
@@ -58,7 +58,7 @@ export async function generateTypes(
// Diff the compiled types against the existing types file
try {
const existingTypes = fs.readFileSync(outputFile, 'utf-8')
const existingTypes = await fs.readFile(outputFile, 'utf-8')
if (compiled === existingTypes) {
return
@@ -67,7 +67,7 @@ export async function generateTypes(
// swallow err
}
fs.writeFileSync(outputFile, compiled)
await fs.writeFile(outputFile, compiled)
if (shouldLog) {
logger.info(`Types written to ${outputFile}`)
}

View File

@@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'fs/promises'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
@@ -34,7 +34,7 @@ export const deleteAssociatedFiles: (args: Args) => Promise<void> = async ({
try {
if (await fileExists(fileToDelete)) {
fs.unlinkSync(fileToDelete)
await fs.unlink(fileToDelete)
}
} catch (err) {
throw new ErrorDeletingFile(req.t)
@@ -50,7 +50,7 @@ export const deleteAssociatedFiles: (args: Args) => Promise<void> = async ({
const sizeToDelete = `${staticPath}/${size.filename}`
try {
if (await fileExists(sizeToDelete)) {
fs.unlinkSync(sizeToDelete)
await fs.unlink(sizeToDelete)
}
} catch (err) {
throw new ErrorDeletingFile(req.t)

View File

@@ -1,8 +1,8 @@
import fs from 'fs'
import fs from 'fs/promises'
const fileExists = async (filename: string): Promise<boolean> => {
try {
await fs.promises.stat(filename)
await fs.stat(filename)
return true
} catch (err) {

View File

@@ -2,8 +2,7 @@
import type { OutputInfo, Sharp, SharpOptions } from 'sharp'
import { fileTypeFromBuffer } from 'file-type'
import fs from 'fs'
import { mkdirSync } from 'node:fs'
import fs from 'fs/promises'
import sanitize from 'sanitize-filename'
import type { Collection } from '../collections/config/types.js'
@@ -121,7 +120,7 @@ export const generateFileData = async <T>({
}
if (!disableLocalStorage) {
mkdirSync(staticPath, { recursive: true })
await fs.mkdir(staticPath, { recursive: true })
}
let newData = data
@@ -291,7 +290,7 @@ export const generateFileData = async <T>({
}
if (file.tempFilePath) {
await fs.promises.writeFile(file.tempFilePath, croppedImage) // write fileBuffer to the temp path
await fs.writeFile(file.tempFilePath, croppedImage) // write fileBuffer to the temp path
} else {
req.file = fileForResize
}
@@ -304,7 +303,7 @@ export const generateFileData = async <T>({
// If using temp files and the image is being resized, write the file to the temp path
if (fileBuffer?.data || file.data.length > 0) {
if (file.tempFilePath) {
await fs.promises.writeFile(file.tempFilePath, fileBuffer?.data || file.data) // write fileBuffer to the temp path
await fs.writeFile(file.tempFilePath, fileBuffer?.data || file.data) // write fileBuffer to the temp path
} else {
// Assign the _possibly modified_ file to the request object
req.file = {

View File

@@ -1,6 +1,6 @@
// @ts-strict-ignore
import { fileTypeFromFile } from 'file-type'
import fs from 'fs'
import fs from 'fs/promises'
import path from 'path'
import type { PayloadRequest } from '../types/index.js'
@@ -11,9 +11,9 @@ const mimeTypeEstimate = {
export const getFileByPath = async (filePath: string): Promise<PayloadRequest['file']> => {
if (typeof filePath === 'string') {
const data = fs.readFileSync(filePath)
const data = await fs.readFile(filePath)
const mimetype = fileTypeFromFile(filePath)
const { size } = fs.statSync(filePath)
const { size } = await fs.stat(filePath)
const name = path.basename(filePath)
const ext = path.extname(filePath).slice(1)

View File

@@ -1,5 +1,5 @@
// @ts-strict-ignore
import fs from 'fs'
import fs from 'fs/promises'
import sizeOfImport from 'image-size'
import { promisify } from 'util'
@@ -21,7 +21,7 @@ export async function getImageSize(file: PayloadRequest['file']): Promise<Probed
if (file.mimetype === 'image/tiff') {
const dimensions = await temporaryFileTask(
async (filepath: string) => {
fs.writeFileSync(filepath, file.data)
await fs.writeFile(filepath, file.data)
return imageSizePromise(filepath)
},
{ extension: 'tiff' },

View File

@@ -2,7 +2,7 @@
import type { Sharp, Metadata as SharpMetadata, SharpOptions } from 'sharp'
import { fileTypeFromBuffer } from 'file-type'
import fs from 'fs'
import fs from 'fs/promises'
import sanitize from 'sanitize-filename'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
@@ -478,7 +478,7 @@ export async function resizeAndTransformImageSizes({
if (await fileExists(imagePath)) {
try {
fs.unlinkSync(imagePath)
await fs.unlink(imagePath)
} catch {
// Ignore unlink errors
}

View File

@@ -1,5 +1,5 @@
// @ts-strict-ignore
import fs from 'fs'
import fs from 'fs/promises'
import { Readable } from 'stream'
/**
@@ -16,7 +16,7 @@ const saveBufferToFile = async (buffer: Buffer, filePath: string): Promise<void>
streamData = null
}
// Setup file system writable stream.
return fs.writeFileSync(filePath, buffer)
return await fs.writeFile(filePath, buffer)
}
export default saveBufferToFile

View File

@@ -1,5 +1,5 @@
// @ts-strict-ignore
import { promises as fsPromises } from 'fs'
import fs from 'fs/promises'
import os from 'node:os'
import path from 'node:path'
import { v4 as uuid } from 'uuid'
@@ -8,7 +8,7 @@ async function runTask(temporaryPath: string, callback) {
try {
return await callback(temporaryPath)
} finally {
await fsPromises.rm(temporaryPath, { force: true, maxRetries: 2, recursive: true })
await fs.rm(temporaryPath, { force: true, maxRetries: 2, recursive: true })
}
}
@@ -41,11 +41,11 @@ async function temporaryFile(options: Options) {
async function temporaryDirectory({ prefix = '' } = {}) {
const directory = await getPath(prefix)
await fsPromises.mkdir(directory)
await fs.mkdir(directory)
return directory
}
async function getPath(prefix = ''): Promise<string> {
const temporaryDirectory = await fsPromises.realpath(os.tmpdir())
const temporaryDirectory = await fs.realpath(os.tmpdir())
return path.join(temporaryDirectory, prefix + uuid())
}

View File

@@ -1,5 +1,4 @@
import fs from 'fs'
import { promisify } from 'util'
import fs from 'fs/promises'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
@@ -7,8 +6,6 @@ import type { PayloadRequest } from '../types/index.js'
import { mapAsync } from '../utilities/mapAsync.js'
const unlinkFile = promisify(fs.unlink)
type Args = {
collectionConfig: SanitizedCollectionConfig
config: SanitizedConfig
@@ -28,7 +25,7 @@ export const unlinkTempFiles: (args: Args) => Promise<void> = async ({
await mapAsync(fileArray, async ({ file }) => {
// Still need this check because this will not be populated if using local API
if (file?.tempFilePath) {
await unlinkFile(file.tempFilePath)
await fs.unlink(file.tempFilePath)
}
})
}