James Ives
James Ives

Reputation: 3365

Changing background-image property causes a flicker in Firefox

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

Answers (2)

Ivica
Ivica

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

eiko
eiko

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

Related Questions