Prefácio
Tenho feito otimização de desempenho recentemente e os métodos específicos de otimização estão por toda a Internet, então não vou repeti-los aqui.
A otimização de desempenho pode ser dividida nas seguintes dimensões: nível de código, nível de construção e nível de rede.
Este artigo explora principalmente o desempenho do front-end no nível do código e está dividido principalmente nas quatro seções a seguir.
-
Use CSS em vez de JS
-
Análise aprofundada de JS
-
Algoritmo de front-end
-
Camada inferior do computador
Use CSS em vez de JS
Aqui o apresentamos principalmente a partir de dois aspectos: animação e componentes CSS.
Animação CSS
Antes do lançamento do CSS2, até mesmo uma animação simples precisava ser implementada por meio de JS. Por exemplo, o movimento horizontal do quadrado vermelho abaixo:
Código JS correspondente:
let redBox = document.getElementById('redBox')
let l = 10
setInterval(() => {
l+=3
redBox.style.left = `${l}px`
}, 50)
A especificação CSS2 de 1998 definiu algumas propriedades de animação, mas devido às limitações da tecnologia do navegador da época, esses recursos não eram amplamente suportados e aplicados.
Até a introdução do CSS3, as animações CSS eram mais totalmente suportadas. Ao mesmo tempo, CSS3 também introduz mais efeitos de animação, tornando a animação CSS amplamente utilizada no desenvolvimento Web atual.
Então, quais animações podem ser obtidas com CSS3, aqui estão alguns exemplos:
-
Transição - Transição é um dos efeitos de animação comumente usados em CSS3. Ao transformar certos atributos de um elemento, o elemento pode fazer uma transição suave de um estado para outro dentro de um período de tempo.
-
Animação - Animação é outro efeito de animação comumente usado em CSS3. É usado para adicionar alguns efeitos de animação complexos a um elemento. Uma série de sequências de animação pode ser definida por meio de quadros-chave (@keyframes).
-
Transform - Transform é uma tecnologia usada em CSS3 para obter efeitos de transformação gráfica 2D/3D, incluindo rotação, dimensionamento, movimento, chanfro e outros efeitos.
Reescreva o exemplo acima em código CSS da seguinte forma:
#redBox {
animation: mymove 5s infinite;
}
@keyframes mymove
{
from {left: 0;}
to {left: 200px;}
}
O mesmo efeito pode ser alcançado usando estilos, então por que não?
Deve-se ressaltar que as animações CSS ainda estão se desenvolvendo e melhorando. Com o surgimento de novos recursos de navegador e versões CSS, os recursos das animações CSS são constantemente adicionados e otimizados para atender às necessidades de animação cada vez mais complexas e melhor experiência do usuário.
Componentes CSS
Em algumas bibliotecas de componentes bem conhecidas, a maioria dos adereços de alguns componentes são implementados modificando estilos CSS, como o componente Space de Vant .
Adereços | Função | Estilos CSS |
---|---|---|
direção | direção de espaçamento | direção flexível: coluna; |
alinhar | Alinhamento | alinhar itens: xxx; |
preencher | Se deve tornar o Espaço um elemento de nível de bloco e preencher todo o elemento pai | exibição: flexível; |
enrolar | Se as linhas devem ser quebradas automaticamente | flex-wrap: envoltório; |
Outro exemplo é o componente Space do Ant Design .
Adereços | Função | Estilos CSS |
---|---|---|
alinhar | Alinhamento | alinhar itens: xxx; |
direção | direção de espaçamento | direção flexível: coluna; |
tamanho | tamanho do espaçamento | lacuna: xxx; |
enrolar | Se as linhas devem ser quebradas automaticamente | flex-wrap: envoltório; |
Este tipo de componente pode ser completamente encapsulado na implementação do mixin SCSS (o mesmo vale para LESS), o que pode não apenas reduzir o volume de construção do projeto (o tamanho do componente Space das duas bibliotecas após gzip é 5,4k e 22,9 k respectivamente), mas também melhoram o desempenho.
Visualize o volume de um componente na biblioteca de componentes e acesse a conexão .
Por exemplo, o seguinte mixin espacial:
/*
* 间距
* size: 间距大小,默认是 8px
* align: 对齐方式,默认是 center,可选 start、end、baseline、center
* direction: 间距方向,默认是 horizontal,可选 horizontal、vertical
* wrap: 是否自动换行,仅在 horizontal 时有效,默认是 false
*/
@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {
display: inline-flex;
gap: $size;
@if ($direction == 'vertical') {
flex-direction: column;
}
@if ($align == 'center') {
align-items: center;
}
@if ($align == 'start') {
align-items: flex-start;
}
@if ($align == 'end') {
align-items: flex-end;
}
@if ($align == 'baseline') {
align-items: baseline;
}
@if ($wrap == true) {
@if $direction == 'horizontal' {
flex-wrap: wrap;
}
}
}
Componentes semelhantes incluem Grade, Layout, etc.
Vamos falar sobre ícones. Abaixo está a primeira captura de tela do componente de ícone do Ant Design. Existem muitos que podem ser facilmente implementados usando apenas HTML + CSS.
Ideias de implementação:
-
Priorize a implementação usando apenas estilos
-
Se o estilo por si só não for suficiente, adicione uma tag primeiro e implemente-a através desta tag e seus dois pseudoelementos::before e::after
-
Se um rótulo não for suficiente, considere adicionar rótulos adicionais.
Por exemplo, para implementar um triângulo sólido que suporta quatro direções, você pode conseguir isso com apenas algumas linhas de estilos (a captura de tela acima mostra 4 ícones):
/* 三角形 */
@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {
width: 0;
height: 0;
border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;
$doubleBorderWidth: 2 * $borderWidth;
$borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};
@if($direction == 'up') {
border-bottom: $borderStyle;
}
@if($direction == 'down') {
border-top: $borderStyle;
}
@if($direction == 'left') {
border-right: $borderStyle;
}
@if($direction == 'right') {
border-left: $borderStyle;
}
}
Resumindo, o que pode ser implementado com CSS não requer JS , além de ter bom desempenho, atravessa pilhas de tecnologia e até cross-terminais.
Análise aprofundada de JS
Depois de apresentar o CSS, vamos dar uma olhada no JS, principalmente sob dois aspectos: instruções básicas e código-fonte da estrutura.
Otimização de instruções if-else
Primeiro entenda como a CPU executa instruções condicionais. Consulte o seguinte código:
const a = 2
const b = 10
let c
if (a > 3) {
c = a + b
} else {
c = 2 * a
}
O fluxo de execução da CPU é o seguinte:
Vemos que quando a instrução 0102 é executada, porque a condição a > 3 não é atendida, ele salta diretamente para a instrução 0104 para execução; além disso, o computador é muito inteligente, se durante a compilação descobrir que a nunca pode ser maior que 3, ele excluirá diretamente a instrução 0103, e então a instrução 0104 se tornará a próxima instrução, que será executada diretamente em sequência, que é a otimização do compilador.
Então, voltando ao tópico, se houver o seguinte código:
function check(age, sex) {
let msg = ''
if (age > 18) {
if (sex === 1) {
msg = '符合条件'
} else {
msg = ' 不符合条件'
}
} else {
msg = '不符合条件'
}
}
A lógica é muito simples. É filtrar pessoas com idade > 18 e sexo == 1. Não há nenhum problema com o código, mas é muito detalhado. Do ponto de vista da CPU, duas operações de salto precisam ser ser executado. Quando idade > 18 anos, entre no if-else interno para continuar julgando, o que significa pular novamente.
Na verdade, podemos otimizar diretamente essa lógica (geralmente fazemos isso, mas podemos saber, mas não sabemos por quê):
function check(age, sex){
if (age > 18 && sex ==1) return '符合条件'
return '不符合条件'
}
Portanto, se a lógica puder terminar mais cedo , ela terminará mais cedo para reduzir saltos de CPU.
Otimização da instrução Switch
Na verdade, não há muita diferença entre a instrução switch e a instrução if-else, exceto que elas são escritas de maneiras diferentes.No entanto, a instrução switch possui uma otimização especial, ou seja, matrizes.
Consulte o seguinte código:
function getPrice(level) {
if (level > 10) return 100
if (level > 9) return 80
if (level > 6) return 50
if (level > 1) return 20
return 10
}
Nós mudamos para uma instrução switch:
function getPrice(level) {
switch(level)
case 10: return 100
case 9: return 80
case 8:
case 7:
case 6: return 50
case 5:
case 4:
case 3:
case 2:
case 1: return 20
default: return 10
}
Parece que não há diferença, mas na verdade o compilador irá otimizá-lo em um array, onde os subscritos do array vão de 0 a 10. O preço correspondente aos diferentes subscritos é o valor do retorno, ou seja:
E sabemos que os arrays suportam acesso aleatório e são extremamente rápidos, portanto, a otimização do switch do compilador melhorará muito a eficiência de execução do programa, que é muito mais rápido do que executar comandos um por um.
Bem, ainda preciso escrever uma declaração if-else estúpida. Não posso simplesmente escrever todas as opções?
não! Como a otimização de switch do compilador é condicional, ela exige que seu código seja compacto, ou seja, contínuo.
Por que é isso? Porque quero usar um array para otimizar você. Se você não for compacto, por exemplo, seu código é 1, 50, 51, 101, 110, vou criar um array com comprimento de 110 para armazená-lo. Somente essas posições são úteis., não é uma perda de espaço!
Portanto, quando usamos switch, tentamos garantir que o código seja do tipo numérico compacto.
Otimização de instruções de loop
Na verdade, as instruções de loop são semelhantes às instruções condicionais, mas são escritas de maneiras diferentes.O ponto de otimização das instruções de loop é principalmente reduzir as instruções.
Vejamos primeiro como escrever a segunda série:
function findUserByName(users) {
let user = null
for (let i = 0; i < users.length; i++) {
if (users[i].name === '张三') {
user = users[i]
}
}
return user
}
Se o comprimento da matriz for 10.086 e a primeira pessoa se chamar Zhang San, as próximas 10.085 travessias serão em vão e a CPU realmente não será usada como ser humano.
Você não poderia simplesmente escrever assim:
function findUserByName(users) {
for (let i = 0; i < users.length; i++) {
if (users[i].name === '章三') return users[i]
}
}
Isto é altamente eficiente na escrita e altamente legível, e também está em linha com a nossa visão acima mencionada de que se a lógica puder terminar mais cedo, terminará mais cedo. CPU agradece a todos diretamente.
Na verdade, há algo que pode ser otimizado aqui, ou seja, o comprimento do nosso array pode ser extraído sem precisar acessá-lo todas as vezes, ou seja:
function findUserByName(users) {
let length = users.length
for (let i = 0; i < length; i++) {
if (users[i].name === '章三') return users[i]
}
}
Isso pode parecer um pouco complicado, e é, mas se você considerar o desempenho, ainda é útil. Por exemplo, a função size() de algumas coleções não é um simples acesso a atributos, mas precisa ser calculada uma vez a cada vez.Este cenário é uma grande otimização, pois salva o processo de muitas chamadas de função, o que significa salvar Existem muitos instruções de chamada e retorno, o que simplesmente melhora a eficiência do código. Especialmente no caso de declarações em loop, onde mudanças quantitativas levam facilmente a mudanças qualitativas, a lacuna é ampliada a partir deste detalhe.
Referência do processo de chamada de função:
O código correspondente é o seguinte:
let a = 10
let b = 11
function sum (a, b) {
return a + b
}
Depois de falar sobre algumas declarações básicas, vamos dar uma olhada na estrutura que usamos com frequência. Vale a pena explorar o desempenho em muitos lugares.
algoritmo de comparação
Tanto o Vue quanto o React usam DOM virtual. Ao realizar atualizações, compare o DOM virtual antigo e o novo. Sem qualquer otimização, a complexidade de tempo de diferenciar estritamente duas árvores diretamente é O (n ^ 3), o que não é utilizável de forma alguma. Portanto, Vue e React devem usar o algoritmo diff para otimizar o DOM virtual:
Vue2 - Comparação dupla:
Semelhante à imagem acima:
-
Defina 4 variáveis: oldStartIdx, oldEndIdx, newStartIdx e newEndIdx
-
Determine se oldStartIdx e newStartIdx são iguais
-
Determine se oldEndIdx e newEndIdx são iguais
-
Determine se oldStartIdx e newEndIdx são iguais
-
Determine se oldEndIdx e newStartIdx são iguais
-
Ao mesmo tempo, oldStartIdx e newStartIdx movem-se para a direita; oldEndIdx e newEndIdx movem-se para a esquerda.
Vue3 - Subsequência crescente mais longa:
Todo o processo é otimizado novamente com base na comparação dupla do Vue2. Por exemplo, a captura de tela acima:
-
Primeiro, execute uma comparação dupla e descubra que os dois primeiros nós (A e B) e o último nó (G) são iguais e não precisam ser movidos.
-
Encontre a subsequência crescente mais longa C, D, E (um grupo de nós que inclui filhos antigos e novos, e a ordem mais longa não mudou)
-
Trate a subsequência como um todo, sem nenhuma operação interna. Você só precisa mover F para a frente dela e inserir H atrás dela.
Reagir - mudar apenas para a direita:
O processo de comparação da captura de tela acima é o seguinte:
-
Atravesse Old e salve o mapa subscrito correspondente
-
Atravessando New, o subscrito de b muda de 1 para 0 e não se move (é um deslocamento para a esquerda, não um deslocamento para a direita)
-
O subscrito de c muda de 2 para 1 e não se move (também se move para a esquerda, não para a direita)
-
O subscrito de a muda de 0 para 2, movendo-se para a direita, e os subscritos de b e c são reduzidos em 1.
-
As posições de d e e não mudaram e não precisam ser movidas.
Resumindo, não importa qual algoritmo seja utilizado, seus princípios são:
-
Compare apenas no mesmo nível, não entre níveis
-
Se a Tag for diferente, exclua-a e reconstrua-a (não compare mais detalhes internos)
-
Os nós filhos são diferenciados por chave (importância da chave)
No final, a complexidade do tempo foi reduzida com sucesso para O(n) antes que pudesse ser usada em nossos projetos reais.
setState é realmente assíncrono?
Muitas pessoas pensam que setState é assíncrono, mas veja o exemplo a seguir:
clickHandler = () => {
console.log('--- start ---')
Promise.resolve().then(() => console.log('promise then'))
this.setState({val: 1}, () => {console.log('state...', this.state.val)})
console.log('--- end ---')
}
render() {
return <div onClick={this.clickHandler}>setState</div>
}
Resultado real da impressão:
Se for assíncrono, a impressão do estado deverá ser executada após a microtarefa Promise.
Para explicar esse motivo, devemos primeiro entender o mecanismo de eventos em JSX.
Eventos em JSX, como onClick={() => {}}, são na verdade chamados de eventos sintéticos, que são diferentes dos eventos personalizados que costumamos chamar:
// 自定义事件
document.getElementById('app').addEventListener('click', () => {})
Os eventos sintéticos estão vinculados ao nó raiz e têm pré e pós-operações. Veja o exemplo acima:
function fn() { // fn 是合成事件函数,内部事件同步执行
// 前置
clickHandler()
// 后置,执行 setState 的 callback
}
Você pode imaginar que existe uma função fn e os eventos nela são executados de forma síncrona, incluindo setState. Após a execução de fn, o evento assíncrono começa a ser executado, ou seja, Promise.then, que é consistente com o resultado impresso.
Então, por que o React faz isso?
Por questões de desempenho, se o estado precisar ser modificado várias vezes, o React mesclará essas modificações primeiro e renderizará o DOM apenas uma vez após a mesclagem, para evitar renderizar o DOM toda vez que for modificado.
Portanto, setState é de natureza síncrona, e o "assíncrono" de que costumamos falar não é rigoroso.
Algoritmo de front-end
Depois de falar do nosso desenvolvimento diário, vamos falar da aplicação de algoritmos no front-end.
Lembrete amigável: Os algoritmos geralmente são projetados para grandes volumes de dados, que são diferentes do desenvolvimento diário.
Se você puder usar tipos de valor, não precisará de tipos de referência.
Vejamos primeiro uma questão.
Encontre todos os números simétricos entre 1-10000, por exemplo: 0, 1, 2, 11, 22, 101, 232, 1221...
Idéia 1 - Use reversão e comparação de arrays: converta números em strings e depois converta-os em arrays; inverta arrays e junte-os em strings; compare as strings antes e depois.
function findPalindromeNumbers1(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
// 转换为字符串,转换为数组,再反转,比较
const s = i.toString()
if (s === s.split('').reverse().join('')) {
res.push(i)
}
}
return res
}
Idéia 2 - Comparação de início e fim de string: Converta números em strings; compare caracteres de início e fim de string.
function findPalindromeNumbers2(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
const s = i.toString()
const length = s.length
// 字符串头尾比较
let flag = true
let startIndex = 0 // 字符串开始
let endIndex = length - 1 // 字符串结束
while (startIndex < endIndex) {
if (s[startIndex] !== s[endIndex]) {
flag = false
break
} else {
// 继续比较
startIndex++
endIndex--
}
}
if (flag) res.push(res)
}
return res
}
Idéia 3 - Gerar números invertidos: Use % e Math.floor para gerar números invertidos; compare os números antes e depois (números operacionais do começo ao fim, sem tipo de string).
function findPalindromeNumbers3(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
let n = i
let rev = 0 // 存储翻转数
// 生成翻转数
while (n > 0) {
rev = rev * 10 + n % 10
n = Math.floor(n / 10)
}
if (i === rev) res.push(i)
}
return res
}
Análise de desempenho: ficando mais rápido
-
Idéia 1- Parece ser O(n), mas a conversão e operação do array levam tempo, por isso é lento
-
Ideia 2 VS Ideia 3 – Manipule números mais rápido (o protótipo do computador é uma calculadora)
Resumindo, tente não converter estruturas de dados, especialmente estruturas ordenadas, como arrays, e tente não usar APIs integradas, como reversas. É difícil identificar a complexidade. As operações numéricas são as mais rápidas, seguidas por strings.
Tente usar código de "baixo nível"
Vamos direto para a próxima pergunta.
Insira uma string e troque as letras maiúsculas e minúsculas nela.
Por exemplo, insira a string 12aBc34 e produza a string 12AbC34.
Ideia 1 – Use expressões regulares.
function switchLetterCase(s) {
let res = ''
const length = s.length
if (length === 0) return res
const reg1 = /[a-z]
const reg2 = /[A-Z]
for (let i = 0; i < length; i++) {
const c = s[i]
if (reg1.test(c)) {
res += c.toUpperCase()
} else if (reg2.test(c)) {
res += c.toLowerCase()
} else {
res += c
}
}
return res
}
Ideia 2 – Julgar pelo código ASCII.
function switchLetterCase2(s) {
let res = ''
const length = s.length
if (length === 0) return res
for (let i = 0; i < length; i++) {
const c = s[i]
const code = c.charCodeAt(0)
if (code >= 65 && code <= 90) {
res += c.toLowerCase()
} else if (code >= 97 && code <= 122) {
res += c.toUpperCase()
} else {
res += c
}
}
return res
}
Análise de desempenho: a primeira usa regularização e é mais lenta que a segunda
Portanto, tente usar código de "baixo nível" e use açúcar sintático, APIs de alto nível ou expressões regulares com cautela.
Camada inferior do computador
Finalmente, vamos falar sobre alguns dos aspectos subjacentes do computador que o front-end precisa entender.
Ler dados da "memória"
O que costumamos dizer: ler dados da memória significa ler dados em registradores.No entanto, nossos dados não são lidos diretamente da memória para os registradores, mas são lidos primeiro em um cache e depois lidos nos registradores.
O registrador está dentro da CPU e também faz parte da CPU, então a CPU lê e grava dados do registrador muito rapidamente.
Por que é isso? Porque a leitura de dados da memória é muito lenta.
Você pode entender desta forma: a CPU primeiro lê os dados no cache para uso e, quando é realmente usado, lê o registrador do cache; quando o registrador é usado, ele grava os dados de volta no cache e então O cache grava os dados na memória no momento apropriado.
A velocidade de operação da CPU é muito rápida, mas a leitura de dados da memória é muito lenta. Se você ler e gravar dados da memória todas as vezes, isso inevitavelmente diminuirá a velocidade de operação da CPU. A execução pode levar 100 segundos e 99 segundos serão gastos lendo dados. Para resolver este problema, colocamos um cache entre a CPU e a memória, e a velocidade de leitura e gravação entre a CPU e o cache é muito rápida. A CPU apenas lê e grava dados no cache, independentemente do cache e o cache.Como sincronizar dados entre memórias. Isso resolve o problema de leitura e gravação lenta na memória.
Operações de bits binários
O uso flexível de operações de bits binários pode não apenas aumentar a velocidade, mas o uso proficiente de binários também pode economizar memória.
Se um número n for dado, como determinar se n é 2 elevado à enésima potência?
É muito simples, basta pedir o restante.
function isPowerOfTwo(n) {
if (n <= 0) return false
let temp = n
while (temp > 1) {
if (temp % 2 != 0) return false
temp /= 2
}
return true
}
Bem, não há nada de errado com o código, mas não é bom o suficiente. Dê uma olhada no código abaixo:
function isPowerOfTwo(n) {
return (n > 0) && ((n & (n - 1)) == 0)
}
Você pode usar console.time e console.timeEnd para comparar a velocidade de execução.
Também podemos ver que existem muitas variáveis de sinalizadores em alguns códigos-fonte. Realizamos operações AND bit a bit ou OR bit a bit nesses sinalizadores para detectar os sinalizadores e determinar se uma determinada função está habilitada. Por que ele simplesmente não usa valores booleanos? É muito simples, é eficiente e economiza memória.
Por exemplo, este código no código-fonte Vue3 não usa apenas AND bit a bit e OR bit a bit, mas também usa deslocamento para a esquerda:
export const enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
...
}
if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS
} else {
if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS
}
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE
}
if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
}
Conclusão
O artigo explica o desempenho do front-end no nível do código, com dimensões detalhadas:
-
Análise aprofundada do conhecimento básico de JS
-
Código-fonte da estrutura
Existem também dimensões de largura:
-
Animações CSS, componentes
-
algoritmo
-
Camada inferior do computador
Espero que possa ajudar todos a ampliar seus horizontes em termos de desempenho de front-end. Se você estiver interessado no artigo, deixe uma mensagem para discussão ~~~
Multado em 200 yuans e mais de 1 milhão de yuans confiscados You Yuxi: A importância dos documentos chineses de alta qualidade A migração radical de servidores de Musk O controle de congestionamento TCP salvou a Internet Apache OpenOffice é um projeto de fato “sem manutenção” Google comemora seu 25º aniversário Microsoft windows-drivers-rs de código aberto, use Rust para desenvolver drivers do Windows Raspberry Pi 5 será lançado no final de outubro, ao preço de US$ 60 macOS Containers: use o Docker para executar imagens do macOS no macOS IntelliJ IDEA 2023.3 EAP lançadoAutor: JD Retail Yang Jinjun
Fonte: JD Cloud Developer Community Por favor, indique a fonte ao reimprimir