fix: optimize metadata payload size and file response parsing for record index pageload
https://sonarly.com/issue/22045?type=bug The /objects/products record index page takes ~6 seconds to load due to a 494KB metadata response, a 5.7MB file download during the pageload window, and a 335ms main thread block from parsing a large response as text. Fix: **What changed:** Modified `fetchCsvPreview` to use streaming reads via `ReadableStream` instead of downloading the entire file with `response.text()`. **Why:** The original code called `response.text()` which downloads the entire file body (potentially many MB) and converts it to a string on the main thread. For a 5.7MB CSV file, this blocks the main thread for ~335ms (observed in the Sentry trace as a Long Animation Frame with invoker `Response.text.then`). The fix uses `response.body.getReader()` to read only up to 512KB of the response — more than enough for 50 rows of CSV preview — then cancels the stream. This avoids downloading the full file and eliminates the main thread blocking. **Changes:** 1. `fetchCsvPreview.ts`: Added `readPartialResponseText()` helper that reads from the response body stream up to `MAX_PREVIEW_BYTES` (512KB), then cancels the stream. Falls back to `response.text()` if the body stream is not available. 2. `fetchCsvPreview.test.ts`: Updated the `mockFetch` helper to provide a `body` ReadableStream alongside `text()`, matching the real `Response` API shape. **Note:** This fix addresses the main-thread-blocking file download for CSV preview. The broader performance issues (large metadata payloads, sequential metadata requests, high-latency user in Japan) are architectural concerns that require larger changes beyond this PR's scope.
This commit is contained in:
+10
@@ -1,9 +1,19 @@
|
||||
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
|
||||
|
||||
const mockFetch = (text: string) => {
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(text);
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoded);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve(text),
|
||||
body: stream,
|
||||
} as unknown as Response),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,48 @@ import Papa from 'papaparse';
|
||||
|
||||
const DEFAULT_PREVIEW_ROWS = 50;
|
||||
|
||||
// Read only enough bytes from the response stream to extract preview rows,
|
||||
// avoiding downloading the entire file (which can be many MB) and blocking
|
||||
// the main thread while converting it to text via response.text().
|
||||
const MAX_PREVIEW_BYTES = 1024 * 512; // 512 KB — generous for 50 rows of CSV
|
||||
|
||||
export type CsvPreviewData = {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
};
|
||||
|
||||
const readPartialResponseText = async (
|
||||
response: Response,
|
||||
): Promise<string> => {
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let accumulated = '';
|
||||
|
||||
try {
|
||||
while (accumulated.length < MAX_PREVIEW_BYTES) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
accumulated += decoder.decode(value, { stream: true });
|
||||
}
|
||||
} finally {
|
||||
reader.cancel();
|
||||
}
|
||||
|
||||
return accumulated;
|
||||
};
|
||||
|
||||
export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
const text = await readPartialResponseText(response);
|
||||
|
||||
const result = Papa.parse<string[]>(text, {
|
||||
preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row
|
||||
|
||||
Reference in New Issue
Block a user