Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Image Caching #2493

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

kidroca
Copy link

@kidroca kidroca commented Mar 3, 2023

Description

This PR fixes some subtle inconsistencies around image caching and rendering the same image multiple times

Fixes #2492

Test Strategy

Created a sandbox sample from the pull request, where the bug can no longer happens
https://codesandbox.io/s/rnw-image-flicker-bug-fix-s93ri0

Steps:

  1. Go to https://codesandbox.io/s/rnw-image-flicker-bug-fix-s93ri0
  2. Press the "Add More" button at the top a few time
  3. Images are no longer flickering

Why No Unit Tests

Couldn't easily add unit tests for this change, because the main change is to the ImageLoader.load which doesn't have a test suit, and is mocked in the Image tests

Existing tests cover the changes in the Image component

@codesandbox-ci
Copy link

codesandbox-ci bot commented Mar 3, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 1929474:

Sandbox Source
react-native-web-examples Configuration
RNW-image-flicker-bug-fix PR
RNW-image-flicker-bug-fix PR
RNW-image-flicker-bug-sample Issue #2492

Comment on lines -275 to 289
exports[`components/Image prop "source" is set immediately if the image has already been loaded 1`] = `
exports[`components/Image prop "source" is set immediately if the image has already been loaded 2`] = `
<div
class="css-view-175oi2r r-flexBasis-1mlwlqe r-overflow-1udh08x r-zIndex-417010"
>
<div
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
style="background-image: url(https://google.com/favicon.ico);"
style="background-image: url(https://twitter.com/favicon.ico);"
/>
<img
alt=""
class="css-accessibilityImage-9pa8cd"
draggable="false"
src="https://google.com/favicon.ico"
src="https://twitter.com/favicon.ico"
/>
</div>
Copy link
Author

Choose a reason for hiding this comment

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

The diff is confusing, it does not represent the change correctly

  1. Test case name was updated
  2. This resulted in 1 new snapshot created and 1 obsolete snapshot removed

If we inspect the snapshot (number 2) we see that it correctly renders uriTwo

const uriTwo = 'https://twitter.com/favicon.ico';

Comment on lines -194 to -217
const [state, updateState] = React.useState(() => {
const uri = resolveAssetUri(source);
if (uri != null) {
const isLoaded = ImageLoader.has(uri);
if (isLoaded) {
return LOADED;
}
}
return IDLE;
});
Copy link
Author

Choose a reason for hiding this comment

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

This is a state initializer, it runs only once, because of that the component does not behave correctly when we switch source, cases like

  • component is mounted with null source then changed to actual source that was already loaded - state was already initialized and is not updated to LOADED
  • component changes source dynamically to some other source that was loaded - same problem as above

This causes flickers on some occasions

Comment on lines +194 to +219
const [state, updateState] = React.useState(IDLE);
const [layout, updateLayout] = React.useState({});
const hasTextAncestor = React.useContext(TextAncestorContext);
const hiddenImageRef = React.useRef(null);
const filterRef = React.useRef(_filterId++);
const requestRef = React.useRef(null);
const uri = resolveAssetUri(source);
const isCached = uri != null && ImageLoader.has(uri);
const shouldDisplaySource =
state === LOADED || (state === LOADING && defaultSource == null);
state === LOADED ||
isCached ||
(state === LOADING && defaultSource == null);
Copy link
Author

Choose a reason for hiding this comment

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

We can capture the old "state initializer" in a hook and run it on source change, so that if already loaded source is passed we can directly switch to LOADED state, but I find it simpler to always start from IDLE state and capture isCached like the proposed change. This way we've evaluated the correct result on the same render, while an use effect would only update state on the next render

Comment on lines 76 to +78
const ImageLoader = {
abort(requestId: number) {
let image = requests[`${requestId}`];
clear(requestId: number) {
const image = requests[`${requestId}`];
Copy link
Author

@kidroca kidroca Mar 3, 2023

Choose a reason for hiding this comment

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

renamed this to clear to represent more closely that it's not just for aborting but actually a necessary cleanup, as it also updates the ImageUriCache now

The change doesn't alter existing behavior

@kidroca
Copy link
Author

kidroca commented Mar 22, 2023

BTW This PR also fixes Image.queryCache - I don't think we have an issue for this, but right now Image.queryCache does not work as intended (nothing already loaded is considered cached)
@necolas do you want me to open an issue for tracking purposes?

@necolas
Copy link
Owner

necolas commented Mar 29, 2023

Hi, please could you rebase this. Now that 0.19 is done, the 0.20 release will be focused on Image changes.

If the Image component is rendered with a `null` source, and consecutively
updated with actual source url that was already loaded, it would fail to
pic kup the change - `state` would be `IDLE` for a brief moment and
this would cause a small flicker when the image renders

Let's always start from IDLE state, and update `shouldDisplaySource`
condition to be based on `ImageLoader.has` cache or not
@kidroca
Copy link
Author

kidroca commented Apr 4, 2023

✅ rebased

Comment on lines 251 to 254
test('is not set immediately if the image has not already been loaded', () => {
const uri = 'https://google.com/favicon.ico';
test('is set immediately while images is loading and there is no default source', () => {
const uri = 'https://google.com/not-yet-loaded-image.ico';
const source = { uri };
const { container } = render(<Image source={source} />);
Copy link
Author

Choose a reason for hiding this comment

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

Perhaps this was originally true, and source was not rendered until loaded, but if you inspect the snapshot you can see that <img /> with the source was rendered, so I renamed the test

We do render the source while loading, unless there's a default source

const shouldDisplaySource =
state === LOADED || (state === LOADING && defaultSource == null);

@necolas
Copy link
Owner

necolas commented Apr 5, 2023

One unit test snapshot is failing

As we can see by the created snapshot - the image is rendered

This is because there's no default source. In such case we
render the Image source while it loads
@kidroca
Copy link
Author

kidroca commented Apr 6, 2023

One unit test snapshot is failing

Sorry, should be fine now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has: pr Subject of a pull request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding already rendered image has a small flicker
2 participants