Skip to content

Defer image loading and use original receipt image - #97313

Merged
mountiny merged 9 commits into
Expensify:mainfrom
bernhardoj:feat/deferred-image-loading
Aug 11, 2026
Merged

Defer image loading and use original receipt image#97313
mountiny merged 9 commits into
Expensify:mainfrom
bernhardoj:feat/deferred-image-loading

Conversation

@bernhardoj

@bernhardoj bernhardoj commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed Issues

$ #96999
PROPOSAL:

Tests

Same as QA Steps

  • Verify that no errors appear in the JS console

Offline tests

Same as QA Steps

QA Steps

Prerequisite: have multiple expenses with receipts, small and large images

  1. Open the expenses one by one
  2. Observe the receipt image
  3. Verify it will load the lower-res image first with a loading and dimmed image
  4. Verify it shows the original image after the loading is done
  5. (WEB only) Hover over the image
  6. Verify the original image is zoomed

large image.jpg

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
web.mp4

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/DeferredImageWithLoading.tsx 100.00% <100.00%> (ø)
src/components/ImageWithLoading.tsx 73.52% <100.00%> (-0.76%) ⬇️
src/components/ImageWithSizeCalculation.tsx 100.00% <100.00%> (ø)
src/components/ReceiptImage/index.tsx 68.88% <0.00%> (ø)
...ponents/ReportActionItem/ReportActionItemImage.tsx 70.96% <0.00%> (-1.17%) ⬇️
... and 15 files with indirect coverage changes

@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

I'm unsure if I'm testing it right, but it looks blurry then it loads in the unpixelated one is that correct?

It doesn't feel horrible or anything. I wonder if we could make it feel slightly better by applying a blur effect until it's fully loaded or even making it grayscale or something. Maybe it's a dumb idea, but I wonder if on worse connections that it'll be slow and they'll think we're loading in a really bad resolution then it suddenly swaps to the hires one. Curious what @Expensify/design thinks

@bernhardoj

bernhardoj commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

but it looks blurry then it loads in the unpixelated one is that correct

Yes, the image switching is actually an existing behavior. But in this PR, we switch from the low-res to the full-res. In prod, we switch from low-res to a higher one, but still blurry. Another new behavior in this PR is that no image is shown until the navigation transition is done.

@dannymcclain

Copy link
Copy Markdown
Contributor

Yeah I'm not sure... For some of my receipts it took many seconds to swap to the higher res version, so it looked like the receipt was just blurry. And it seems like the height of the receipt preview area changes once the higher res version loads in? I wonder if it would be better to just spinner the whole thing until we can load the good version? It's not awful by any means, but it doesn't feel super great to me.

But also if this is essentially an upgrade of current behavior I guess we can proceed and keep trying to make it better.

@mountiny

Copy link
Copy Markdown
Contributor

I think the delay is sort of inevitable as if the high resolution receipt is large, then its just taking a while to download couple megabytes and hence the delay. Only thing we could do is to show some loading feedback instead of just a blury larger low res image until the larger res loads

@dannymcclain

Copy link
Copy Markdown
Contributor

Only thing we could do is to show some loading feedback instead of just a blury larger low res image until the larger res loads

Yeah that could definitely help. Even if it's a little unobtrusive thing that still let's you see the low res image in the background (so not totally blocking, but just gives you some affordance)

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Yeah, that's kinda what I was getting at. I'm aware the delay is by design, but I think the transition and general "waiting state" can be better. Do we have a way to actually understand when the new image is loaded in? If so, I wonder if we could dim the receipt with a spinner on top and then un-dim with the high resolutiojn receipt

@bernhardoj

Copy link
Copy Markdown
Contributor Author

Do we have a way to actually understand when the new image is loaded in? If so, I wonder if we could dim the receipt with a spinner on top and then un-dim with the high resolutiojn receipt

Yes, we do. We actually already have a loading, but it's only shown after 200ms and if the thumbnail (the lower res) is still loading.

{isLoading && (!previewUri || isThumbnailLoading) && !isImageCached && !isOffline && (
<LoadingIndicator
iconSize={loadingIconSize}
style={[styles.opacity1, styles.bgTransparent, loadingIndicatorStyles]}
/>
)}

We can simplify it and shows whenever the higher-res image is loading.

Here is how it looks (with the dim):

web.mp4

@dannymcclain

Copy link
Copy Markdown
Contributor

Here is how it looks (with the dim):

The last few giant ones in your video still seem a bit weird*, but the first few with normal images actually felt pretty good to me.

*Why is the receipt preview area so massive? I thought it would only ever be as tall as the viewport available. Also I didn't see the spinner on those—is that just because it's out of view because the preview area is so massive?

Does seem like this could be a good solution though because it at least sets users expectations a bit better than just a blurry image.

@bernhardoj

Copy link
Copy Markdown
Contributor Author

*Why is the receipt preview area so massive? I thought it would only ever be as tall as the viewport available.

This is an existing behavior. Maybe it was intentionally designed like that?

Also I didn't see the spinner on those—is that just because it's out of view because the preview area is so massive?

Yes, it's at the center of the image and since the image is really long, we need to scroll to the middle of the image to see it.

@dannymcclain

dannymcclain commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

*Why is the receipt preview area so massive? I thought it would only ever be as tall as the viewport available.

This is an existing behavior. Maybe it was intentionally designed like that?

This doesn't sound right to me. @Expensify/design what do y'all think? (Even so, this sounds like a separate issue from the one we're focusing on here.)

@shawnborton

Copy link
Copy Markdown
Contributor

Hmm I think we did design it that way at one point (to cap the max-height) but then we got some weirdness with how to display the receipt in terms of using something similar to background: cover, etc. I guess the reality is that most receipts are pretty standard size and we don't run into this often. I'd be down to do nothing here or follow up and do something with these edge cases if you feel strongly!

Everything else seems to be looking nice to me though.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

I'd be down to do nothing here or follow up and do something with these edge cases if you feel strongly!

No strong feelings. Fine with following up. Might wanna do some prototyping for this and have clearer instructions 👍

@bernhardoj

Copy link
Copy Markdown
Contributor Author

Cool. @mountiny, so should we go with that approach?

@dannymcclain

Copy link
Copy Markdown
Contributor

No strong feelings here! Happy to proceed.

@bernhardoj

Copy link
Copy Markdown
Contributor Author

@mountiny cool, looks like everyone is happy to proceed with the approach. Should I continue this PR and let a C+ review this or let Melvin create the PR and I review it?

@mountiny

mountiny commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

yeah if the design team is happy with this approach lets do it

@bernhardoj

Copy link
Copy Markdown
Contributor Author

So this receipt image component is being used in several places, for example, here, in the report preview

image

or when viewing the expense on a small screen.

image

Both usages pass shouldUseThumbnailImage as true to the component. Do we also want to use the original image there or keep using the thumbnail when shouldUseThumbnailImage is true?

@shawnborton

Copy link
Copy Markdown
Contributor

Probably fine to keep using the thumbnail versions?

@mountiny
mountiny self-requested a review August 9, 2026 18:19
@mountiny

mountiny commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot @codex review

@thesahindia can you review this PR on Monday or is your plate full? thanks!

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63354a8585

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

shouldUseThumbnailImage: shouldUseThumbnailImage ?? true,

source: thumbnailSource,
source: shouldUseThumbnailImage !== false ? thumbnailSource : originalImageSource,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep remote PDFs off the image loader

When the wide receipt panel passes shouldUseThumbnailImage={false}, this now sends originalImageSource even for remote PDF receipts. ReceiptImage then routes that .pdf URL into DeferredImageWithLoading/ImageWithLoading, which is the image pipeline, while the existing receipt URI helper intentionally provides a generated .jpg.1024.jpg thumbnail for PDFs and PDF rendering is handled by separate PDF components. On tablet/native or any no-hover context where the web-only PDF overlay is not mounted, opening a remote PDF receipt in the full-height receipt panel drops the JPG preview and leaves the receipt blank after the image load fails; keep the thumbnail source for PDFs or render a PDF component instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@github-actions

This comment has been minimized.

@thesahindia

thesahindia commented Aug 10, 2026

Copy link
Copy Markdown
Member

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
Screen.Recording.2026-08-11.at.2.00.33.AM.mov
iOS: HybridApp
iOS: mWeb Safari
Screen.Recording.2026-08-11.at.2.43.30.AM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-08-11.at.12.51.59.AM.mov

@joekaufmanexpensify joekaufmanexpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for product

@thesahindia thesahindia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works well!

@mountiny mountiny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pushing this ahead ❤️

@mountiny
mountiny merged commit 5e89148 into Expensify:main Aug 11, 2026
38 of 41 checks passed
@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.53-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 I reviewed the changes in this PR and no help site changes are required under docs/articles.

Why: This PR is an internal image-rendering improvement — it defers image loading during screen transitions (new DeferredImageWithLoading wrapper) and serves the original higher-resolution image for non-PDF receipts so hover-to-zoom is sharper. It introduces no new user-facing feature, setting, tab, button, or workflow. The behavior it improves (receipt preview loading and zoom quality) isn't documented in the help site — a search of docs/articles for hover, zoom, receipt image, image quality, and resolution returned no matching content, so there's nothing to update.

Since there are no required doc updates, I did not create a draft help site PR. @bernhardoj, if you believe a specific help article should be updated to reflect this change, let me know which one and I'll draft it.

@jponikarchuk

Copy link
Copy Markdown

This PR failing because of the issue #98566
This issue is reproducible in: Android app

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #98587 was identified to be related to this PR.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.53-10 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.