From 5675bd4e089a9cec39a3fa581cd8acafab63d71f Mon Sep 17 00:00:00 2001 From: "mykola.gervasyuk" Date: Thu, 20 Aug 2026 13:48:27 +0300 Subject: [PATCH 1/2] perf: serve images from a static host instead of the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API serves images from the same single-threaded process that computes image diffs, so during build ingestion image requests queue behind CPU-bound work and screenshots take seconds to load. The UI container's nginx already exposes the imageUploads volume, so let deployments opt in via REACT_APP_STATIC_URL (e.g. /static/imageUploads) to fetch images from it directly. When unset, images keep going through the API — S3-backed deployments are unaffected. --- .env | 1 + src/_config/env.config.ts | 4 ++++ src/services/static.service.ts | 10 +++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 7e5d14ab..a4b7ec00 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ REACT_APP_API_URL=http://localhost:4200 +REACT_APP_STATIC_URL= PORT=8080 VRT_VERSION=5.0.0 \ No newline at end of file diff --git a/src/_config/env.config.ts b/src/_config/env.config.ts index e30611b3..df2be8a3 100644 --- a/src/_config/env.config.ts +++ b/src/_config/env.config.ts @@ -1,10 +1,14 @@ export const API_URL = window._env_?.REACT_APP_API_URL; +// Optional base URL for serving images directly from a static host (e.g. the +// nginx of the UI container). When unset, images are fetched through the API. +export const STATIC_URL = window._env_?.REACT_APP_STATIC_URL; export const VRT_VERSION = window._env_?.VRT_VERSION; declare global { interface Window { _env_: { REACT_APP_API_URL: string; + REACT_APP_STATIC_URL?: string; VRT_VERSION: string; }; } diff --git a/src/services/static.service.ts b/src/services/static.service.ts index 62d3b2d1..cba49115 100644 --- a/src/services/static.service.ts +++ b/src/services/static.service.ts @@ -1,11 +1,15 @@ -import { API_URL } from "../_config/env.config"; +import { API_URL, STATIC_URL } from "../_config/env.config"; import noImage from "../static/no-image.png"; import JSZip from "jszip"; import FileSaver from "file-saver"; function getImage(name: string): string { - if (name) return `${API_URL}/images/${name}`; - return noImage; + if (!name) return noImage; + // Prefer the static host: the API serves images from the same single-threaded + // process that computes image diffs, so under ingestion load image requests + // queue behind CPU-bound work. + if (STATIC_URL) return `${STATIC_URL}/${name}`; + return `${API_URL}/images/${name}`; } async function downloadAsZip( From 11f1d59d02e67c624799968cb9f9d4274b0ed69d Mon Sep 17 00:00:00 2001 From: "mykola.gervasyuk" Date: Thu, 20 Aug 2026 14:32:30 +0300 Subject: [PATCH 2/2] perf: skip test-run socket events that target another build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Socket events arrive for every build, and even an all-filtered-out batch dispatched a state update that replaced the testRuns array — invalidating every memo over it and re-rendering the grid each debounce tick while an unrelated build was ingesting. With thousands of runs open that made the whole UI stutter. Skip empty batches, return the same state when nothing matched, and deduplicate by id via Set/Map instead of nested scans. --- src/contexts/socket.context.tsx | 36 ++++++++++++++-------- src/contexts/testRun.context.tsx | 52 ++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/contexts/socket.context.tsx b/src/contexts/socket.context.tsx index 2388abb3..dcfbc6b7 100644 --- a/src/contexts/socket.context.tsx +++ b/src/contexts/socket.context.tsx @@ -95,10 +95,16 @@ function SocketProvider({ children }: SocketProviderProps) { return; } - addTestRun( - testRunDispatch, - testRuns.filter((tr) => tr.buildId === selectedBuild.id), + // events for other builds arrive too — an empty dispatch would still + // replace the (possibly huge) testRuns array and re-render the grid + const created = testRuns.filter( + (tr) => tr.buildId === selectedBuild.id, ); + if (created.length === 0) { + return; + } + + addTestRun(testRunDispatch, created); }); state.socket.on("testRun_updated", function (testRuns: TestRun[]) { @@ -106,10 +112,14 @@ function SocketProvider({ children }: SocketProviderProps) { return; } - updateTestRun( - testRunDispatch, - testRuns.filter((tr) => tr.buildId === selectedBuild.id), + const updated = testRuns.filter( + (tr) => tr.buildId === selectedBuild.id, ); + if (updated.length === 0) { + return; + } + + updateTestRun(testRunDispatch, updated); }); state.socket.on("testRun_deleted", function (testRuns: TestRun[]) { @@ -117,12 +127,14 @@ function SocketProvider({ children }: SocketProviderProps) { return; } - deleteTestRun( - testRunDispatch, - testRuns - .filter((tr) => tr.buildId === selectedBuild.id) - .map((testRun) => testRun.id), - ); + const deleted = testRuns + .filter((tr) => tr.buildId === selectedBuild.id) + .map((testRun) => testRun.id); + if (deleted.length === 0) { + return; + } + + deleteTestRun(testRunDispatch, deleted); }); } }, [ diff --git a/src/contexts/testRun.context.tsx b/src/contexts/testRun.context.tsx index 93b01b19..44ac6edd 100644 --- a/src/contexts/testRun.context.tsx +++ b/src/contexts/testRun.context.tsx @@ -117,40 +117,46 @@ function testRunReducer(state: State, action: IAction): State { loading: false, }; - case "delete": + case "delete": { + const deletedIds = new Set(action.payload); return { ...state, - testRuns: state.testRuns.filter((p) => !action.payload.includes(p.id)), + testRuns: state.testRuns.filter((p) => !deletedIds.has(p.id)), }; + } - case "add": + case "add": { + const existingIds = new Set(state.testRuns.map((tr) => tr.id)); + // remove duplicates + const added = action.payload.filter((i) => !existingIds.has(i.id)); + if (added.length === 0) { + return state; + } return { ...state, - testRuns: [ - ...state.testRuns, - ...action.payload.filter( - // remove duplicates - (i) => !state.testRuns.find((tr) => tr.id === i.id), - ), - ], + testRuns: [...state.testRuns, ...added], }; + } - case "update": + case "update": { + const updatedById = new Map(action.payload.map((i) => [i.id, i])); + const selectedUpdate = state.selectedTestRun + ? updatedById.get(state.selectedTestRun.id) + : undefined; + // keep the testRuns reference stable when nothing matched: replacing the + // array invalidates every memo over a possibly huge run list + if ( + !selectedUpdate && + !state.testRuns.some((t) => updatedById.has(t.id)) + ) { + return state; + } return { ...state, - testRuns: state.testRuns.map((t) => { - const item = action.payload.find((i) => i.id === t.id); - - if (item) { - return item; - } - - return t; - }), - selectedTestRun: - action.payload.find((i) => i.id === state.selectedTestRun?.id) ?? - state.selectedTestRun, + testRuns: state.testRuns.map((t) => updatedById.get(t.id) ?? t), + selectedTestRun: selectedUpdate ?? state.selectedTestRun, }; + } case "touched": return {