Skip to content

fix(web): skip thumbhash fade for offscreen-loaded thumbnails#26846

Closed
midzelis wants to merge 2 commits intomainfrom
push-kulzwwtvyqkk
Closed

fix(web): skip thumbhash fade for offscreen-loaded thumbnails#26846
midzelis wants to merge 2 commits intomainfrom
push-kulzwwtvyqkk

Conversation

@midzelis
Copy link
Copy Markdown
Collaborator

See #20773 as the original source/inspiration

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 2026

Preview environment has been removed.

$effect(() => {
if (loaded && !loadedEffectRan) {
loadedEffectRan = true;
if (!actuallyIntersecting) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we really need this check? Elements are only added to the DOM if they're visible or near visible already?

Copy link
Copy Markdown
Collaborator Author

@midzelis midzelis Mar 11, 2026

Choose a reason for hiding this comment

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

This check skips the thumbhash fade-out. Elements are only added to the DOM when they are within +/- 500px of the viewport (intersecting) or in the viewport itself. actuallyIntersecting means the element is actually IN the viewport. So when !actuallyIntersecting is true at load time, the thumbnail loaded within the offscreen margins. In that case, we skip the thumbhash fade-out — the placeholder is instantly hidden.

Before this change, the 100ms fade-out would play offscreen, and would almost always complete before the element scrolled into view. This change eliminates the edge case where fast scrolling (>500px/100ms) could cause you to see a partial thumbhash fade-out on an element that loaded in the margins.

5000px/s is fast but achievable. In practice, any partial fade at these scroll speeds would be too brief to notice — but this change eliminates it entirely.

@uhthomas
Copy link
Copy Markdown
Collaborator

I don't think this is quite aligned with the original intention of the changes.

If you scroll up and down in search, you will always see a thumbhash and fade when the assets come into view at either the top or bottom depending on direction. There's no real reason for this other than the async nature of the web APIs. The images should be cached locally and should load instantly, but they don't.

https://pr-26846.preview.internal.immich.build/search?query=%7B%22query%22%3A%22dog%22%7D

@uhthomas
Copy link
Copy Markdown
Collaborator

IDK if you can see this, but just look at the top of the video behind the search bar on the preview environment for this PR. The image always fades in when scrolling.

2026-03-11_14-06-02.mp4

@midzelis
Copy link
Copy Markdown
Collaborator Author

midzelis commented Mar 11, 2026

I don't think this is quite aligned with the original intention of the changes.

If you scroll up and down in search, you will always see a thumbhash and fade when the assets come into view at either the top or bottom depending on direction. There's no real reason for this other than the async nature of the web APIs. The images should be cached locally and should load instantly, but they don't.

This change was meant for timeline - I didn't realize you were referring to search results view. Search uses a very different system for displaying the images/thumbnails - it would have to be adapted for that.

@midzelis midzelis force-pushed the push-kulzwwtvyqkk branch 2 times, most recently from 7c4f27e to 1ec63b4 Compare March 12, 2026 02:16
@midzelis midzelis requested a review from uhthomas March 12, 2026 02:16
export const Intersection = {
NONE: 0,
PRE: 1,
ACTUAL: 3, // includes PRE (both bits set)
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.

what is PRE?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pre intersecting. 500 pc before or after the viewport. Used to preload up-and-coming thumbs. Means thumb is about to be onscreen. Also known as "intersecting".

"Actual" represents "actuallyIntersecting" which means the thumb is actually on screen.

Copy link
Copy Markdown
Member

@mertalev mertalev Mar 14, 2026

Choose a reason for hiding this comment

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

I think the whole “intersecting vs actually intersecting” wording is confusing personally. Intersecting should mean “actually intersecting”, the other one should have a clearer term like “near viewport” or something.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In my last push, using flags (NONE,NEARBY,VISIBLE) and a derived flag RENDERABLE which is NEARBY | VISIBLE.

Struggled with naming, but I think RENDERABLE makes sense as a composition of NEARBY and VISIBLE. Its not JUST visible - since it includes NEARBY. NEARBY_OR_VISIBLE was a little too long. And, I can't use the past tense, since its not YET rendered (thats would be when it actually goes into the DOM). Also considered IN_DOM (but again, that wouldn't be accurate) or DOM_ELIGIBLE - but RENDERABLE was my preferred.

This was a fairly big change, as it trickled down to a lot of places that use intersecting/isIntersecting/actuallyIntersecting.

I also improved/consolidated the intersection calculation code in intersection-support - and its a little more performant by flipping the conditions for NONE first.

These changes were in a separate commit, so they can be broken out into another PR if you prefer.

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.

naming is getting harder and harder ngl 😅

@midzelis midzelis force-pushed the push-kulzwwtvyqkk branch from 1ec63b4 to 8440a82 Compare March 16, 2026 14:41
@midzelis midzelis requested a review from alextran1502 March 16, 2026 14:55
@midzelis midzelis force-pushed the push-kulzwwtvyqkk branch 2 times, most recently from 3563d2c to 1df73f2 Compare March 19, 2026 03:59
Thumbnails that finish loading while offscreen (in the 500px pre-load
zone) were still playing the thumbhash fade-out transition, causing
visual flicker when scrolling. This adds an `actuallyIntersecting`
property (zero-margin viewport check) alongside the existing
`intersecting` (500px expanded margins) to distinguish between
pre-loaded and truly visible assets.

- Refactor `calculateViewerAssetIntersecting` to return a numeric flag
  (NONE/PRE/ACTUAL) avoiding object allocation in the hot derived path
- Inline `calculateMonthGroupIntersecting` into
  `updateIntersectionMonthGroup` to avoid intermediate object allocation
- Thread `actuallyIntersecting` through AssetLayout → Month → Timeline
  → Thumbnail snippet chain
- Use `actuallyIntersecting` in Thumbnail to skip the fade when loaded
  offscreen
- Add unit tests for `isIntersecting` and
  `calculateViewerAssetIntersecting`
…ersecting

Change-Id: Id6f63247441fe290e7732e489f73e8c16a6a6964
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants