Reputation: 3365
I'm working on a component that rotates a series of background images in a banner on my page. The problem I'm running into is that when the background-image
properties url is changed via state it seems to cause a flash of white. This flashing doesn't seem to happen all the time in Chrome, but does happen consistently in Firefox and sometimes Safari. For additional context I'm using Mac OSX.
At first I assumed this was because the images are being retrieved by the browser when they are requested, but to avoid this I've made some considerations for pre-fetching by rendering a hidden image tag with the resource.
{this.props.items.map(item => (
<img src={item} style={{ display: "none" }} />
))}
I've also tried creating a new image in the rotate
method that pre-fetches the next rotation item ahead of the transition, but neither seem to work.
const img = new Image();
img.src = this.props.items[index + 1];
Where am I going wrong here? I've attached an example of the component below. Any help would be appreciated.
class Cats extends React.Component {
constructor(props) {
super(props);
this.state = {
background: props.items[0],
index: 0
};
this.rotate = this.rotate.bind(this);
}
// Let's you see when the component has updated.
componentDidMount() {
this.interval = setInterval(() => this.rotate(), 5000);
}
componentDidUnmount() {
clearInterval(this.interval);
}
rotate() {
const maximum = this.props.items.length - 1;
const index = this.state.index === maximum ? 0 : this.state.index + 1;
this.setState({
background: this.props.items[index],
index
});
}
render() {
return (
<div
className="background"
style={{ backgroundImage: `url(${this.state.background})` }}
>
{this.props.items.map(item => (
<img src={item} style={{ display: "none" }} />
))}
</div>
);
}
}
ReactDOM.render(
<Cats
items={[
"https://preview.redd.it/8lt2w3du0zb31.jpg?width=640&crop=smart&auto=webp&s=58d0eb6771296b3016d85ee1828d1c26833fd022",
"https://preview.redd.it/120qmpjmg1c31.jpg?width=640&crop=smart&auto=webp&s=1b01fc0c3f20098e6bb1f4126c3c2a54b7bc2b8e",
"https://preview.redd.it/guprqpenoxb31.jpg?width=640&crop=smart&auto=webp&s=ace24e96764bb40a01e7d167a88d35298db76a1c",
"https://preview.redd.it/mlzq0x1o0xb31.jpg?width=640&crop=smart&auto=webp&s=b3fd159069f45b6c354de975daffde21f04c3ad5"
]}
/>,
document.querySelector(".wrapper")
);
html, body, .wrapper {
width: 100%;
height: 100%;
}
.background {
position: static;
background-size: cover;
height: 100%;
width: 100%;
transition: background-image 1s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.1/react-dom.min.js"></script>
<div class="wrapper"></div>
Upvotes: 4
Views: 2105
Reputation: 386
You can use decode()
method that will let you know when image is decoded and ready to be used.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode
In your case:
const img = new Image();
img.src = this.props.items[index + 1];
img.decode()
.then(() => {
// image is decoded and ready to use
})
.catch((encodingError) => {
// do something with the error.
})
Upvotes: 4
Reputation: 5345
Unfortunately, it seems like this flicker is a known bug in Firefox caused by its image decoder, which won't decode an image until it's displayed for the first time. In the snippet below, I created overlapping divs, one which loads the next image slightly earlier and sits behind the other. This way when the other "flickers," the proper image is already displayed behind, rather than a white background.
You could also theoretically display all the images in the hidden div really quickly, then set it back to white, since the images only need to be displayed once for the decoder to work.
Depending on the long-term goal for this project, the most proper way around this problem may be to use a <canvas>
to render your images. The canvas element uses a different decoder which won't cause a flicker.
class Cats extends React.Component {
constructor(props) {
super(props);
this.props.items.forEach((item) => {
const img = new Image(640, 640);
img.src = item;
});
this.state = {
background: props.items[0],
preloadBackground: props.items[1],
index: 0
};
this.rotate = this.rotate.bind(this);
}
// Let's you see when the component has updated.
componentDidMount() {
this.interval = setInterval(() => this.rotate(), 5000);
}
componentDidUnmount() {
clearInterval(this.interval);
}
rotate() {
const maximum = this.props.items.length - 1;
const index = this.state.index === maximum ? 0 : this.state.index + 1;
this.setState({
preloadBackground: this.props.items[index],
index
});
setTimeout(() => {
this.setState({
background: this.props.items[index],
});
}, 100);
}
render() {
return (
<div className="pane">
<div
className="preload-background"
style={{ backgroundImage: `url(${this.state.preloadBackground})` }}
>
</div>
<div
className="background"
style={{ backgroundImage: `url(${this.state.background})` }}
>
</div>
</div>
);
}
}
ReactDOM.render(
<Cats
items={[
"https://preview.redd.it/8lt2w3du0zb31.jpg?width=640&crop=smart&auto=webp&s=58d0eb6771296b3016d85ee1828d1c26833fd022",
"https://preview.redd.it/120qmpjmg1c31.jpg?width=640&crop=smart&auto=webp&s=1b01fc0c3f20098e6bb1f4126c3c2a54b7bc2b8e",
"https://preview.redd.it/guprqpenoxb31.jpg?width=640&crop=smart&auto=webp&s=ace24e96764bb40a01e7d167a88d35298db76a1c",
"https://preview.redd.it/mlzq0x1o0xb31.jpg?width=640&crop=smart&auto=webp&s=b3fd159069f45b6c354de975daffde21f04c3ad5"
]}
/>,
document.querySelector(".wrapper")
);
html, body, .wrapper, .pane {
width: 100%;
height: 100%;
}
.background {
position: static;
background-size: cover;
height: 100%;
width: 100%;
transition: background-image 1s ease-in-out;
}
.preload-background {
position: absolute;
background-size: cover;
height: 100%;
width: 100%;
z-index: -1;
transition: background-image 1s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.1/react-dom.min.js"></script>
<div class="wrapper"></div>
Upvotes: 4