Olá Habr! Meu nome é Kamo Spertsyan, estou envolvido no desenvolvimento do React Native no Profi.ru. Se você decidir usar a tecnologia React Native para fornecer rapidamente recursos do produto e se concentrar na velocidade de desenvolvimento, é provável que encontre problemas de desempenho. Pelo menos foi o que aconteceu conosco. Após seis meses de desenvolvimento ativo, o desempenho de nosso aplicativo caiu abaixo de um nível crítico - tudo ficou muito lento. Portanto, adotamos a otimização - removemos todos os "freios" durante a inicialização, transições entre telas, renderizando telas, reações às ações do usuário. Como resultado, em três meses eles trouxeram a experiência do usuário para o nível nativo. Neste artigo, quero falar sobre como otimizamos o aplicativo no React Native e resolvemos o problema de renderizações de vários componentes.
Reuni recomendações que ajudarão a minimizar o número de redesenhos inúteis de componentes. Para maior clareza, nos exemplos eu comparo as implementações "ruim" e "boa". O artigo será útil para aqueles que já enfrentam um desempenho ruim do aplicativo e para aqueles que não desejam permitir isso no futuro.Usamos React Native emparelhado com Redux. Algumas dicas estão relacionadas a esta biblioteca. Também no exemplo, eu uso a biblioteca Redux-thunk - para simular o trabalho com a rede.Quando pensar em desempenho?
De fato, vale lembrar desde o início do trabalho no aplicativo. Mas se o seu aplicativo já estiver diminuindo a velocidade - não se desespere, tudo pode ser corrigido.Todo mundo sabe, mas só para garantir, vou mencionar: é melhor verificar o desempenho em dispositivos fracos. Se você estiver desenvolvendo em dispositivos avançados, talvez não conheça os "freios" dos usuários finais. Decida por si mesmo os dispositivos pelos quais você será guiado. Meça o tempo ou o FPS nos gráficos de controle para comparar com os resultados após a otimização.Reagir nativo imediatamente fornece a capacidade de medir aplicativos FPS através das Ferramentas do desenvolvedor → Mostrar monitor de desempenho. O valor de referência é 60 quadros por segundo. Quanto menor esse indicador, mais forte o aplicativo "fica mais lento" - não responde ou reage com um atraso às ações do usuário. Um dos principais efeitos no FPS é o número de renderizações, cuja “gravidade” depende da complexidade dos componentes.Descrição do exemplo
Eu mostro todas as recomendações no exemplo de um aplicativo simples com uma lista de notícias. O aplicativo possui uma tela, localizada FlatList
com as notícias. Um item de notícia é um componente NewsItem
que consiste em dois componentes menores - o título ( NewsItemTitle
) e o corpo ( NewsItemBody
). O exemplo inteiro pode ser visto aqui . Além disso, no texto há links para várias ramificações do repositório para exemplos específicos. O repositório é usado para a conveniência dos leitores que desejam explorar exemplos mais profundamente. O código no repositório e nos exemplos abaixo não afirma ser perfeito - é necessário apenas para fins de demonstração.Abaixo, todos os componentes são mostrados esquematicamente com links e acessórios.No método de renderização de cada componente, adicionei a saída ao console de informações exclusivas sobre ele:SCREEN
ITEM_{no}
ITEM_TITLE_{no}
ITEM_BODY_{no}
onde {no}
é o número de série do noticiário para distinguir entre diferentes renderizações de notícias e várias renderizações do mesmo.Para testar em cada refresh
lista de notícias, são adicionadas notícias adicionais ao seu início. Ao mesmo tempo, a seguinte mensagem é exibida no console:--------------[ REFRESHING ]--------------
Esses registros ajudarão a entender se há um problema em algum componente específico e, posteriormente, a determinar se foi possível otimizá-lo.Se implementado corretamente, nosso log após o lançamento e várias atualizações devem ficar assim:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
--------------[ REFRESHING ]--------------
SCREEN
ITEM_4
ITEM_TITLE_4
ITEM_BODY_4
No primeiro início, a tela em si e duas notícias iniciais são sorteadas. Ao atualizar o quadro, a tela é renderizada novamente, porque seus dados realmente mudaram. Mais notícias aparecem. Todas as notícias anteriores não são redesenhadas, pois não houve alterações nos dados.Quando um componente é renderizado?
Em React e React Native, há duas condições para renderizar um componente:- mudando seus adereços / estado,
- render do componente pai.
Uma função pode ser redefinida em um componente shouldComponentUpdate
- ela recebe novos Props e State como uma entrada e informa se o componente deve ser renderizado. Freqüentemente, para evitar re-renderizações desnecessárias, basta uma comparação superficial dos objetos Props e State. Por exemplo, isso elimina renderizações desnecessárias quando o componente pai é alterado, se eles não afetam o componente filho. Para não escrever uma comparação de superfície manualmente a cada vez, você pode herdar um componente React.PureComponent
que encapsula essa verificação.Quando usamos a função link de conexão, a biblioteca Redux cria um novo componente que é "conectado" ao estado global. Alterações nesse estado acionam um métodomapStateToProps
que retorna novos adereços. Em seguida, é iniciada uma comparação de adereços antigos e novos, independentemente de o componente ter sido declarado PureComponent
ou não.Considere essas nuances em nosso exemplo.Vamos NewsItem
deixar o componente passar connect
, NewsItemTitle
herdar de React.Component
e NewsItemBody
- de React.PureComponent
.→ Código de exemplo completoexport class NewsItemTitle extends React.Component
export class NewsItemBody extends React.PureComponent
A seguir, como será o registro após uma atualização de quadro:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
Você pode ver que os componentes de notícias e títulos são redesenhados. Vamos considerá-los por sua vez.NewsItem
declarado usando connect
. Como acessório, esse componente recebe um identificador, pelo qual recebe posteriormente notícias em mapStateToProps
:const mapStateToProps = (state, ownProps) => ({
item: state.newsMap[ownProps.itemKey],
});
Como ao atualizar o painel, todas as notícias são baixadas novamente, o objeto será item
atualizado e depois se referirá a várias células de memória. Em outras palavras, eles serão objetos diferentes, mesmo que todos os campos contidos sejam os mesmos. Portanto, uma comparação do componente State'ov anterior e novo retorna false
. O componente será renderizado novamente, apesar do fato de que os dados não foram alterados.NewsItemTitle
é herdado de React.Component
, portanto, é renderizado novamente sempre que o componente pai é renderizado. Isso acontece independentemente dos valores dos adereços antigos e novos.NewsItemBody
herdado de React.PureComponent
, para comparar adereços antigos e novos. Nas notícias 1 e 2, seus valores são equivalentes; portanto, o componente é renderizado apenas para as notícias 3.Para otimizar as renderizaçõesNewsItemTitle
Apenas declare como React.PureComponent
. No caso de, você NewsItem
deve redefinir a função shouldComponentUpdate
:shouldComponentUpdate(nextProps) {
return !shallowEqual(this.props.item, nextProps.item);
}
→ Código de exemplo completoAqui shallowEqual
está um recurso para comparação de superfície de objetos que o Redux fornece. Você pode escrever assim:shouldComponentUpdate(nextProps) {
return (
this.props.item.title !== nextProps.item.title ||
this.props.item.body !== nextProps.item.body
);
}
Aqui está a aparência do nosso log depois disso:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
NotashouldComponentUpdate
NewsItem
, NewsItemTitle
. . NewsItemTitle
- NewsItem
, .
React.memo e componentes funcionais
Não é shouldComponentUpdate
possível substituir em um componente funcional. Mas isso não significa que, para otimizar um componente funcional, você tenha que reescrevê-lo em uma classe. Para esses casos, a função de memorização React.memo é fornecida . Ele aceita uma entrada de componente e uma função de comparação opcional areEqual
. Quando chamado, ele areEqual
adquire adereços antigos e novos e deve retornar o resultado da comparação. A diferença com o shouldComponentUpdate
que areEqual
deve retornar true
se os adereços forem iguais, e não vice-versa.Por exemplo, a NewsItemTitle
memorização pode ser assim:areEqual(prevProps, nextProps) {
return shallowEqual(prevProps, nextProps);
}
export OptimizedNewsItemTitle = React.memo(NewsItemTitle, areEqual)
Se você não passar areEqual
em React.memo
, em seguida, uma comparação superficial de adereços será feita, então o nosso exemplo pode ser simplificada:export OptimizedNewsItemTitle = React.memo(NewsItemTitle)
Funções Lambda em adereços
Para processar eventos de componentes, as funções podem ser passadas para seus props. O exemplo mais impressionante é a implementação onPress
. Frequentemente, funções lambda anônimas são usadas para isso. Digamos NewsItemBody
que queremos mostrar apenas a visualização e, se você clicar nela - o texto inteiro. Para fazer isso, ao renderizar NewsItem
, NewsItemBody
passaremos o seguinte suporte:<NewsItemBody
...
onPress={() => this.props.expandBody()}
...
/>
Aqui está a aparência do log nesta implementação quando o método shouldComponentUpdate
é NewsItem
excluído:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
Os corpos de notícias 1 e 2 são renderizados, embora seus dados não tenham sido alterados, mas NewsItemBody
são PureComponent
. Isso se deve ao fato de que para cada renderização, o NewsItem
valor de adereços onPress
é criado novamente. Tecnicamente, onPress
com cada renderização, ele aponta para uma nova área na memória, portanto, uma comparação superficial de adereços em NewsItemBody
retornos falsos. O problema foi corrigido pela seguinte entrada:<NewsItemBody
...
onPress={this.props.expandBody}
...
/>
Registro:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
→ Código de exemplo completoInfelizmente, uma função anônima nunca pode ser reescrita como método ou campo de classe para esse registro. O caso mais comum é quando, dentro da função lambda, são utilizadas as variáveis de escopo da função na qual ela é declarada.Considere este caso em nosso exemplo. Para alternar da lista geral para a tela de uma notícia, adicionamos o processamento de clicar no corpo da notícia. O método renderItem
component FlatList
terá a seguinte aparência:const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={() => this.onItemBodyPress(item)}
/>
);
Uma função anônima onBodyPress
não pode ser declarada em uma classe, porque a variável item
necessária para acessar uma notícia específica desaparecerá do escopo .A solução mais simples para o problema é alterar a assinatura dos acessórios do onBodyPress
componente NewsItem
para que o parâmetro necessário seja passado para a função quando chamado. Nesse caso, esse é o identificador de notícias.const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={item => this.onItemBodyPress(item)}
/>
);
Nesse caso, já podemos remover a função anônima no método da classe component.const renderItem = ({item}) => (
<NewsItem
itemKey={item}
onBodyPress={this.onItemBodyPress}
/>
);
No entanto, essa solução exigirá que alteremos o componente NewsItem
.class NewsItemComponent extends React.Component {
render() {
...
return (
...
<NewsItemBody
...
onPress={() => this.props.onBodyPress(this.props.item)}
...
/>
...
);
}
E, novamente, voltamos ao problema indicado - passamos uma nova função lambda para o componente filho para cada renderização do pai. Só agora descemos um nível. Registro:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
Para se livrar desse problema na raiz, você pode usar o gancho useCallback . Permite memorizar uma chamada de função passando um argumento. Se o argumento da função não for alterado, o resultado da chamada useCallback
apontará para a mesma área de memória. No nosso exemplo, isso significa que, ao redesenhar as mesmas notícias, o onPress
componente prop NewsItemBody
não será alterado. Os ganchos só podem ser usados em componentes funcionais; portanto, a aparência final do componente NewsItem
será a seguinte:function NewsItemComponent(props) {
...
const {itemKey, onBodyPress} = props.item;
const onPressBody = useCallback(() => onBodyPress(itemKey), [itemKey, onBodyPress]);
return (
<View>
...
<NewsItemBody
...
onPress={onPressBody}
...
/>
</View>
);
}
E o log:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_1
ITEM_TITLE_1
→ Código de exemplo completoMatrizes e objetos
Em JavaScript, funções são representadas como objetos, juntamente com matrizes. Portanto, o exemplo do bloco anterior é um caso especial de criação de um novo objeto em props. É bastante comum, então coloquei em um parágrafo separado.Qualquer criação de novas funções, matrizes ou objetos em adereços leva a um re-renderizador de componentes. Considere esta regra no exemplo a seguir. Vamos passar em um NewsItemBody
estilo combinado de dois valores:<NewsItemBody
...
style={[styles.body, styles.item]}
...
/>
E, novamente, o log mostra os renderizadores de componentes extras:SCREEN
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
--------------[ REFRESHING ]--------------
SCREEN
ITEM_3
ITEM_TITLE_3
ITEM_BODY_3
ITEM_2
ITEM_TITLE_2
ITEM_BODY_2
ITEM_1
ITEM_TITLE_1
ITEM_BODY_1
Para resolver esse problema, você pode selecionar um estilo separado que irá combinar body
e item
, ou, por exemplo, mover a declaração da matriz [styles.body, styles.item]
para uma variável global.→ Código de exemplo completoRedutores de matriz
Considere outra fonte popular de "freios" associados ao uso FlatList
. Um aplicativo clássico que contém uma longa lista de itens do servidor implementa a paginação. Ou seja, ele carrega um conjunto limitado de elementos na forma da primeira página, quando a lista de elementos atuais termina, carrega a página seguinte e assim por diante. Um redutor da lista de itens pode ficar assim:const newsIdList = (state = [], action) => {
if (action.type === 'GOT_NEWS') {
return action.news.map(item => item.key);
} else if (action.type === 'GOT_OLDER_NEWS') {
return [...state, ...action.news.map(item => item.key)];
}
return state;
};
Quando cada página seguinte é carregada no estilo do aplicativo, uma nova matriz de identificadores é criada. Se passarmos essa matriz para props mais tarde FlatList
, eis a aparência do log de renderização do componente:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..10>
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
ITEM_<1..30>
Para este exemplo, fiz algumas alterações no aplicativo de teste.- Defina o tamanho da página como 10 notícias.
item
NewsItem
FlatList
-, connect. NewsItem
React.Component
.- .
- . №1 .
O exemplo mostra que, quando cada página seguinte é carregada, todos os elementos antigos são renderizados novamente, então os elementos antigos e os elementos da nova página são renderizados novamente. Para os amantes da matemática: se o tamanho da página for igual X
, quando a i
i - ésima página for carregada, em vez de renderizar apenas X
novos elementos, os elementos serão renderizados (i - 1) * X + i * X
.“Ok”, você diz, “eu entendo por que todos os elementos são desenhados após adicionar uma nova página: o redutor retornou uma nova matriz, uma nova área de memória, tudo isso. Mas por que precisamos renderizar a lista antiga antes de adicionar novos elementos? ” "Boa pergunta", eu vou responder. Isso é consequência do trabalho com o estado do componente VirtualizedList
em cuja baseFlatList
. Não vou entrar em detalhes, pois eles publicam um artigo separado. Quem se importa, eu aconselho você a se aprofundar na documentação e na fonte.Como se livrar de tal não otimização? Reescrevemos o redutor para que ele não retorne uma nova matriz para cada página, mas adicione elementos à existente:Atenção! Antipattern!. , , ,
PureComponent
, . , . .
Redux.
const newsIdList = (state = [], action) => {
if (action.type === 'GOT_NEWS') {
return action.news.map(item => item.key);
} else if (action.type === 'GOT_OLDER_NEWS') {
action.news.forEach(item => state.push(item.key));
return state;
// return [...state, ...action.news.map(item => item.key)];
}
return state;
};
Depois disso, nosso log ficará assim:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<1..30>
Nos livramos da renderização de elementos antigos antes de adicionar elementos a uma nova página, mas os elementos antigos ainda são desenhados após a atualização da lista. O número de renderizações para a próxima página agora é igual i * X
. A fórmula ficou mais simples, mas não vamos parar por aí. Temos apenas X
novos elementos e queremos apenas X
novas renderizações. Usaremos os truques já conhecidos para remover as renderizações de notícias que não mudaram os adereços. Voltar conectar a NewsItem
:SCREEN
ITEM_<1..10>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<11..20>
--------------[ LOADING NEXT PAGE ]--------------
SCREEN
ITEM_<21..30>
Bem! Agora podemos ficar satisfeitos conosco. Não há lugar para otimizar.→ Código de exemplo completoUm leitor atento indicará que, após aplicar a conexão ao NewsItem
log, será semelhante ao último exemplo, não importa como você implemente o redutor. E estará certo - se o componente de notícias verificar seus acessórios antes da renderização, não importa se a matriz antiga é usada pelo redutor ou se ela cria uma nova. Apenas novos elementos são desenhados e apenas uma vez. No entanto, alterar a matriz antiga em vez de criar uma nova nos salva de renderizações desnecessárias do componente FlatList
usado nela VirtualizedList
e de iterações desnecessárias de verificações de equivalência de props NewsItem
. Com um grande número de elementos, isso também aumenta o desempenho.O uso de matrizes e objetos mutáveis nos redutores deve ser extremamente cauteloso. Neste exemplo, isso é justificado, mas se você tiver, digamos, normal PureComponent
, quando adicionar elementos à matriz mutável, os componentes não serão renderizados. De fato, seus objetos permanecem inalterados, pois antes e depois da atualização dos pontos da matriz para a mesma área de memória. Isso pode levar a consequências inesperadas. Não é de admirar que o exemplo descrito viole os princípios do Redux .E algo mais...
Se você usar bibliotecas no nível da apresentação, aconselho que você entenda em detalhes como elas são implementadas. Em nosso aplicativo, usamos um componente Swipeable
da biblioteca react-native-gesture-handler
. Ele permite implementar um bloco de ações adicionais ao trocar cartões da lista.No código, fica assim:<Swipeable
...
renderRightActions={this.renderRightActions}
...
>
Método renderRightActions
ou renderLeftActions
retorna o componente que é exibido após o furto. Determinamos e alteramos a altura do painel durante a troca de componentes para ajustar o conteúdo necessário. Esse é um processo que consome muitos recursos, mas se ocorrer durante a animação de furto, o usuário não verá interferência.O problema é que o componente Swipeable
chama o método renderRightActions
no momento da renderização do componente principal. Todos os cálculos e até a renderização da barra de ação, que não é visível antes do furto, ocorrem com antecedência. Portanto, todas essas ações são executadas para todos os cartões da lista ao mesmo tempo. Isso causou "freios" significativos ao rolar a prancha.O problema foi resolvido da seguinte maneira. Se o painel de ação for desenhado junto com o componente principal, e não como resultado do furto, o método renderRightActions
retornará um vazio do View
tamanho do componente principal. Caso contrário, desenharemos o painel de ações adicionais como antes.Dou este exemplo porque as bibliotecas de suporte nem sempre funcionam como o esperado. E se essas são bibliotecas no nível da apresentação, é melhor garantir que elas não estejam desperdiçando recursos desnecessários.achados
Após eliminar os problemas descritos no artigo, aceleramos significativamente o aplicativo no React Native. Agora é difícil diferenciá-lo no desempenho de um similar, implementado nativamente. O excesso de renderizações diminuiu o carregamento de telas individuais e a reação às ações do usuário. Acima de tudo, era perceptível nas listas, onde dezenas de componentes são desenhados ao mesmo tempo. Não otimizamos tudo, mas as telas principais do aplicativo não ficam mais lentas.Os principais pontos do artigo estão listados abaixo a seguir.- React Native : Props/State- .
- ,
React.PureComponent
, , . - ,
shouldComponentUpdate
React.Memo
. - - . , (shallow compare). , .
- O suporte a bibliotecas no nível da apresentação pode levar ao desperdício inesperado de recursos. Vale a pena ter cuidado em sua aplicação.
Isso é tudo. Espero que você encontre as informações úteis. Ficarei feliz em qualquer feedback!Fontes úteis
- Compreendendo a renderização no React + Redux
- Comparando objetos em JavaScript
- Melhorando o desempenho em reagir componentes funcionais usando React.memo ()
- Como o Discord atinge o desempenho nativo do iOS com o React Native