Criando efeito de Lazy Load em imagens no React Native

Quando lidamos com aplicações mobile, estamos automaticamente expostos à ambientes completamente diferentes para cada usuário. Geralmente quando realizamos os testes da nossa aplicação, estamos conectados em redes de alta velocidade, mal nos damos conta que o usuário pode utilizar o app em redes 3G de baixa velocidade ou, ainda pior, sem internet. Você lida com esses fatores na sua aplicação?

Nesse post vou te ensinar a cuidar de um dos pontos citados acima para as imagens do seu app.

Você já utilizou o Instagram? Já percebeu que as imagens no feed principal possuem um indicador de progresso e que se você estiver em redes de baixa velocidade uma imagem menor com um blur aplicado é mostrada antes mesmo da imagem com qualidade original:

Isso acontece principalmente porque a rede não possui capacidade de exibir as imagens rapidamente e para dar uma melhor experiência ao usuário final, o app realiza uma “amostra” do conteúdo da imagem antes mesmo de exibí-la em tamanho real, esse efeito se chama Lazy Load.

Configurações de Lazy Load

Apesar do conceito ser basicamente o mesmo, existem formas diferentes de aplicar o efeito de Lazy Load e nesse post vamos falar sobre três deles.

Placeholder simples

A forma mais rápida de darmos um efeito bacana no carregamento das nossas imagens é adicionar uma imagem de placeholder antes da imagem original. Nesse caso, é necessário que esse pré-carregamento seja extremamente leve e praticamente instantâneo.

O objetivo que queremos com esse modelo é chegar em algo assim:

Temos uma imagem de placeholder que carrega antes da imagem original e assim que carregada a imagem deve sobrepor o placeholder com um efeito de animação na opacidade. Vamos criar um novo componente que irá fazer a tratativa desse efeito:

/* Core */
import React, { Component } from 'react';

/* Presentational */
import { View, Image, Animated } from 'react-native';

export default class PlaceholderImage extends Component {
  state = {
    opacity: new Animated.Value(0),
  }

  onLoad = event => {
    Animated.timing(this.state.opacity, {
      toValue: 1,
      duration: 300,
    }).start();
  }

  render() {
    return (
      <View
        style={{
          backgroundColor: '#EEE',
          width: this.props.style.width || '100%',
          height: this.props.style.height || '100%',
        }}
      >
        <Image
          {...this.props}
          source={require('./placeholder.png')}
        />
        <Animated.Image
          {...this.props}
          style={[
            this.props.style,
            { position: 'absolute', opacity: this.state.opacity }
          ]}
          onLoad={this.onLoad}
        />
      </View>
    );
  }
}

Nós encapsulamos duas <Image> com uma <View> repassando a largura e altura da imagem para a mesma. A primeira imagem é responsável por mostrar o placeholder guardado localmente enquanto que a segunda imagem é responsável por carregar a imagem mais pesada online.

Assim que a imagem original é carregada ela chama o método onLoad que realiza a transição de opacidade mostrando a imagem por cima do placeholder. Essa transição é feita com a classe Animated disponível no React Native. Como a imagem original possui um position: absolute, ela passa a ocupar o espaço do placeholder.

Placeholder com blur

Para avançar na utilização do placeholder você pode configurar seu backend para retornar a imagem com tamanho muito inferior com um blur aplicado.

Por exemplo, a imagem que utilizo possui 300px de largura e altura, reduzi ela para 100px e apliquei um blur de 10px (esticar a imagem e aplicar o blur pode ser feito pelo React Native).

O efeito final ficou assim:

Carregamento no scroll

Outra técnica muito utilizada é carregar as imagens apenas quando o usuário chega perto delas com o scroll. Pra isso, vamos utilizar o componente <FlatList> unido de sua propriedade onViewableItemsChangedque será chamada toda vez que um novo item aparecer em tela via scroll:

import React, { Component } from 'react';

import { View, FlatList, Image } from 'react-native';

import PlaceholderImage from './PlaceholderImage';

export default class App extends Component {
  state = {
    data: [
      { id: 0, url: 'https://images.unsplash.com/photo-1467703834117-04386e3dadd8', loaded: false },
      { id: 1, url: 'https://images.unsplash.com/photo-1511971523672-53e6411f62b9', loaded: false },
      { id: 2, url: 'https://images.unsplash.com/photo-1494522358652-f30e61a60313', loaded: false },
    ],
  }

  handleLazyLoad = ({ viewableItems }) => {
    const newData = this.state.data.map(image =>
      viewableItems.find(({ item }) => item.id === image.id)
        ? { ...image, loaded: true }
        : image
    );

    this.setState({ data: newData });
  }

  renderItem = ({ item }) => (
    <View style={{ marginVertical: 20, height: 300, backgroundColor: '#EEE' }}>
      { item.loaded && <Image
        source={{ uri: item.url }}
        style={{ width: '100%', height: 300 }}
      /> }
    </View>
  );

  render() {
    return (
      <FlatList
        data={this.state.data}
        renderItem={this.renderItem}
        keyExtractor={item => item.id}
        onViewableItemsChanged={this.handleLazyLoad}
      />
    );
  }
}

No código eu mostro uma lista de imagens com altura fixa de 300px, assim que o scroll alcançar um novo item nos executamos a função handleLazyLoad que percorre os itens visíveis enviados pelo método onViewableItemsChanged e, por fim, alteramos o estado loaded para true em todos items que estiverem nesse vetor. O resultado é algo parecido com isso:

Veja que a última imagem carregou apenas quando chegamos com o scroll nela.

Para facilitar a visualização adicionei um fundo cinza à <View> por volta da imagem. Caso você utilize esse tipo de Lazy Load, o componente por volta da <Image> deve sempre possuir uma altura fixa (no meu caso 300px), isso porquê sem isso, em um primeiro instante, a aplicação irá constar como todos itens visíveis.

Unindo todos conceitos

Unindo os conceitos de placeholder com blur e carregamento no scroll obtemos uma interface semelhante ao Instagram:

In the eeeeend…

it doesn’t even matter…

Chegamos ao fim de mais um post sobre React Native. A ideia de utilizarmos esse efeito de Lazy Load está relacionado diretamente à experiência do usuário na nossa aplicação.

Pode parecer muito trabalho para pouco efeito, mas imagine seu usuário em uma tela com 20 imagens em uma internet 3G.

Agora que você já sabe aplicar os efeitos de Lazy Load no seu app deixa suas palmas aí e um comentário pra eu saber que você realmente gostou do post 🙂