Skip to content

Commit a7cf976

Browse files
authored
Merge pull request Expensify#90670 from callstack-internal/perf/optimize-receipt-thumbnail-gate
perf: Skip thumbnail encode on receipt capture
2 parents 2f5161e + da54b56 commit a7cf976

3 files changed

Lines changed: 33 additions & 19 deletions

File tree

src/CONST/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ const CONST = {
314314
PHOTO_WIDTH: 4032,
315315
PHOTO_HEIGHT: 3024,
316316
PHOTO_ASPECT_RATIO: 4 / 3,
317+
// Limit how long the shutter handler waits for thumbnail pre-generation before navigating
318+
// to the confirmation screen. Past this, we navigate and let the confirm-side hook
319+
// generate the thumbnail lazily (brief source swap is acceptable).
320+
THUMBNAIL_NAV_TIMEOUT_MS: 150,
317321
},
318322

319323
API_ATTACHMENT_VALIDATIONS: {
@@ -2023,6 +2027,7 @@ const CONST = {
20232027
SPAN_ENTRY_TO_SCAN_NAVIGATION: 'ManualEntryToScanNavigation',
20242028
SPAN_ENTRY_TO_SCAN_READY: 'ManualEntryToScanReady',
20252029
SPAN_SHUTTER_TO_CONFIRMATION: 'ManualShutterToConfirmation',
2030+
SPAN_THUMBNAIL_GATE: 'ManualThumbnailGate',
20262031
SPAN_RECEIPT_CAPTURE: 'ManualReceiptCapture',
20272032
SPAN_SCAN_PROCESS_AND_NAVIGATE: 'ManualScanProcessAndNavigate',
20282033
SPAN_CONFIRMATION_MOUNT: 'ManualConfirmationMount',

src/hooks/useLocalReceiptThumbnail.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,26 @@ function releaseUri(uri: string) {
2121
}
2222

2323
/**
24-
* Pre-populate the thumbnail cache so the confirm screen can use it
24+
* Pre-populate the receipt-image cache so the confirm screen can use it
2525
* synchronously on first render, avoiding any source swap / flash.
2626
*/
27-
function pregenerateThumbnail(sourceUri: string): Promise<string | undefined> {
27+
function precacheReceiptImage(sourceUri: string): Promise<string | undefined> {
2828
if (thumbnailCache.has(sourceUri)) {
2929
return Promise.resolve(thumbnailCache.get(sourceUri));
3030
}
31-
return generateThumbnail(sourceUri).then((uri) => {
32-
if (uri) {
33-
thumbnailCache.set(sourceUri, uri);
34-
// Pre-decode the thumbnail in the native image pipeline so the
35-
// confirmation screen can display it instantly without decode latency.
36-
Image.prefetch(uri);
37-
}
38-
return uri;
39-
});
31+
thumbnailCache.set(sourceUri, sourceUri);
32+
// Pre-decode the image in the native image pipeline so the
33+
// confirmation screen can display it instantly without decode latency.
34+
return Image.prefetch(sourceUri)
35+
.then(() => sourceUri)
36+
.catch(() => sourceUri);
4037
}
4138

4239
/**
43-
* Returns a cached low-resolution thumbnail for a local receipt image.
44-
* The thumbnail should be pre-generated via `pregenerateThumbnail` before
45-
* navigating to the confirm screen. If it wasn't, this hook generates it
46-
* as a fallback, but in that case a source swap (flash) may occur.
40+
* Returns a cached receipt image URI for a local receipt. The image should be
41+
* pre-cached via `precacheReceiptImage` before navigating to the confirm screen.
42+
* If it wasn't, this hook generates a thumbnail as a fallback, but in that case
43+
* a source swap (flash) may occur.
4744
*/
4845
function useLocalReceiptThumbnail(sourceUri: string | undefined, isLocalFile: boolean): {thumbnailUri: string | undefined; isGenerating: boolean} {
4946
const [asyncResult, setAsyncResult] = useState<{source: string; uri?: string; done: boolean} | undefined>();
@@ -109,5 +106,5 @@ function useLocalReceiptThumbnail(sourceUri: string | undefined, isLocalFile: bo
109106
return {thumbnailUri, isGenerating};
110107
}
111108

112-
export {pregenerateThumbnail};
109+
export {precacheReceiptImage};
113110
export default useLocalReceiptThumbnail;

src/pages/iou/request/step/IOURequestStepScan/hooks/useCapturePhoto.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Alert} from 'react-native';
44
import {RESULTS} from 'react-native-permissions';
55
import type {Camera, PhotoFile} from 'react-native-vision-camera';
66
import useLocalize from '@hooks/useLocalize';
7-
import {pregenerateThumbnail} from '@hooks/useLocalReceiptThumbnail';
7+
import {precacheReceiptImage} from '@hooks/useLocalReceiptThumbnail';
88
import getPhotoSource from '@libs/fileDownload/getPhotoSource';
99
import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath';
1010
import Log from '@libs/Log';
@@ -203,9 +203,21 @@ function useCapturePhoto({
203203
}
204204

205205
// Fire Onyx merge immediately (non-blocking) while we await thumbnail generation.
206-
// Both run in parallel — navigation proceeds once the thumbnail is cached.
206+
// Limit the wait to THUMBNAIL_NAV_TIMEOUT_MS so a slow encode can't block navigation —
207+
// the confirm-screen hook (`useLocalReceiptThumbnail`) generates lazily on mount as a fallback.
207208
setMoneyRequestReceipt(transactionID, source, filename, !isEditing, 'image/jpeg');
208-
pregenerateThumbnail(source).then(() => {
209+
startSpan(CONST.TELEMETRY.SPAN_THUMBNAIL_GATE, {
210+
name: CONST.TELEMETRY.SPAN_THUMBNAIL_GATE,
211+
op: CONST.TELEMETRY.SPAN_THUMBNAIL_GATE,
212+
parentSpan: getSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION),
213+
});
214+
Promise.race([
215+
precacheReceiptImage(source),
216+
new Promise((resolve) => {
217+
setTimeout(resolve, CONST.RECEIPT_CAMERA.THUMBNAIL_NAV_TIMEOUT_MS);
218+
}),
219+
]).then(() => {
220+
endSpan(CONST.TELEMETRY.SPAN_THUMBNAIL_GATE);
209221
submitReceipts(newReceiptFiles);
210222
});
211223
})

0 commit comments

Comments
 (0)