Como resolver o problema de que o tempo de execução do JS é muito longo e a página congela

fundo

Como todos sabemos, JavaScript (referido como JS) é uma linguagem de thread único. O navegador atribui apenas um thread principal ao JS e executa uma tarefa da fila de tarefas a cada vez até que a fila de tarefas esteja vazia. Isso inevitavelmente fará com que alguns navegadores congelem. Por exemplo, o feedback de back-end desta vez, na tarefa de upload de arquivo, JS precisa cortar o arquivo e enviá-lo para o servidor em pedaços, mas ao encontrar arquivos grandes (centenas de gigabytes ) Às vezes, centenas de milhares de cópias precisam ser fragmentadas e, em seguida, esses fragmentos são gradualmente carregados no servidor. Acho que é porque demora um pouco mais para o JS processar esses fragmentos de arquivo nesse processo, resultando em fps de renderização de página baixos, então a página congela. Então comecei a pensar, quais são as maneiras de resolver o problema de congelamento de página causado pelo longo tempo de execução do JS?

Soluções

De acordo com meu entendimento pessoal de JS, diante desse tipo de problema travado, a solução é a seguinte:

  • Use o Web Worker para abrir novos encadeamentos e processar tarefas longas e complexas por meio de novos encadeamentos, de modo a evitar o congelamento da página causado pelo bloqueio do encadeamento principal.
  • Use a função do gerador para dividir tarefas longas em várias tarefas macro, para não espremê-las todas na fila de microtarefas e afetar a renderização da página.
  • requestAnimationFrame

tarefa longa

Tarefas que exigem que o JS seja executado continuamente dentro de um determinado período de tempo.
O código a seguir simula a situação de JS executando um longo processo

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    @keyframes move {
      
      
        from {
      
      
            left: 0;
        }

        to {
      
      
            left: 100%;
        }
    }

    .move {
      
      
        position: absolute;
        animation: move 5s linear infinite;
    }
</style>

<body>
    <div class="move">LHX-FLY</div>
</body>
<script>
    function longFun() {
      
      
        let i = 0
        const start = performance.now()
        // 5秒内不停执行 i++ 操作,实现长任务效果
        while (performance.now() - start <= 5000) {
      
      
            i++
        }
        return i
    }
    // 1秒后才执行js长任务
    setTimeout(() => {
      
      
        longFun()
    }, 1000)

</script>

O efeito é o seguinte:
insira a descrição da imagem aqui
Podemos ver que depois de atualizar a página, o efeito da animação ainda é suave antes de 1 segundo, mas depois de 1 segundo, porque o JS tem que processar uma tarefa longa, a página não pode ser renderizada e a animação Está preso.

insira a descrição da imagem aqui
Também pode ser observado pelo desempenho do console que o tempo total de execução é superior a 5 segundos, sendo que a partir de 1 segundo a thread principal da CPU está basicamente ocupada, ou seja, a interface para a renderização da animação durante este período de tempo.

Web Worker

Para o problema de longo atraso na tarefa acima, o código da solução do Web Worker é o seguinte:

// 创建线程函数
function createWorker(f) {
    
    
    var blob = new Blob(['(' + f.toString() + ')()']);
    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);
    return worker;
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     createWorker(longFun); // 用新的线程去执行这个长任务
     // longFun()
}, 1000)   

Os resultados da execução são os seguintes: insira a descrição da imagem aqui
insira a descrição da imagem aqui
Podemos ver que depois de atualizar a página, o efeito de animação de toda a interface ainda é muito suave. Embora o tempo total de execução permaneça inalterado, ainda é mais de 5 segundos, mas o tempo de execução do thread principal é de apenas 54 ms, e o desempenho de execução do novo thread é atribuído a Idle, e a CPU nem sempre está ocupada.

função do gerador

Antes de começar a usar a função geradora para resolver o problema, primeiro precisamos entender o processo de execução do JS, ou seja, o processo geral do ciclo de eventos, conforme a figura abaixo: A partir do processo de ciclo de eventos, podemos
insira a descrição da imagem aqui
saber que, salvo casos especiais, a renderização da página ficará na fila de microtarefas Após a limpeza, antes de executar a tarefa de macro. Assim, podemos deixar a função enviada para a pilha de execução principal executar por um determinado período de tempo para dormir e, em seguida, ativá-la na tarefa de macro após a renderização, para que a renderização ou a interação do usuário não fique travada!
Microtask
Promise
process.nextTick
Object.observe
MutaionObserver
...
Macrotask
incluindo script de código geral
setTimeout
setIntervcal
I/O
postMessage
MessageChannel
...

Na verdade, a função do gerador nada mais é do que dividir tarefas longas em etapas, e haverá uma pausa entre cada etapa (uma etapa é uma micro tarefa, quando a função do gerador não executar a seguir, a próxima etapa não será executada, e a fila de micro tarefas será limpa neste momento, a próxima tarefa não será executada até a próxima etapa). A conversão do problema de apelação em uma função geradora tem a seguinte forma:

// 将原来的长任务改成 generator 函数
function* fnc_() {
    
    
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
    
    
        yield i++
    }
    return i
}

// 简易时间分片 
function timeSlice (fnc) {
    
     
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc() 
    
    return async function (...args) {
    
     
        const fnc_ = fnc(...args) 
        let data 
        do {
    
     
            data = fnc_.next()
        } while (!data.done) 
        return data.value 
    } 
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     const fnc = timeSlice(fnc_)
     const start = performance.now()
     console.log('开始')
     const num = await fnc()
     console.log('结束', `${
      
      (performance.now() - start) / 1000}s`)
     console.log(num)
}, 1000)  

A partir dos resultados de desempenho a seguir, pode-se ver que o código de recurso é exatamente o mesmo que o Caton original. O código de recurso apenas transforma a tarefa longa em um iterador gerador e toda a função fnc_ é relativa a uma tarefa de macro e, em seguida, usa a função timeSlice para torná-lo Non-stop next para executar a próxima etapa (cada etapa é uma microtarefa, mas se a próxima etapa não for executada imediatamente, a fila de microtarefas da etapa atual será executada e aguardará a próxima instrução) até que o done seja concluído, mas o tempo total será maior do que Acontece que a execução leva muito tempo. insira a descrição da imagem aqui
insira a descrição da imagem aqui
Mas adicione o seguinte código na próxima linha:

do {
    
    
    data = fnc_.next()
    // 每执行一步就休眠,注册一个宏任务 setTimeout 来叫醒他
    await new Promise(resolve => setTimeout(resolve))
} while (!data.done)

Na verdade, o próximo do gerador é uma operação executada após ser acordado. await new Promise(resolve => setTimeout(resolve)) gera uma tarefa de macro setTimeout, que é para deixá-lo executar o próximo depois (dormir), durante este curto sleep Durante este período, a fila de microtask pode ser considerada limpa, e outras tarefas serão executadas primeiro, e a página também será renderizada. Após executar um loop de evento, deixe-o executar em seguida, para que o tempo de execução contínuo não seja será tão longo, mas será executado Dividido em execução passo a passo, continuará a executar em um loop até que a execução da fila de tarefas seja concluída.
insira a descrição da imagem aqui
Pode-se ver que o efeito de animação é sempre suave
insira a descrição da imagem aqui
, mas a fatia de tempo acima realmente vai dormir a cada etapa, então a eficiência da execução é relativamente baixa e a função de divisão de tempo pode ser otimizada:

// 精准时间分片 
function timeSlice_(fnc, time = 25) {
    
    
    if (fnc.constructor.name !== 'GeneratorFunction') return fnc()
    return function (...args) {
    
    
        const fnc_ = fnc(...args)
        function go() {
    
    
            const start = performance.now()
            let data
            do {
    
    
                data = fnc_.next()
            } while (!data.done && performance.now() - start < time)
            if (data.done) return data.value
            return new Promise((resolve, reject) => {
    
    
                setTimeout(() => {
    
    
                    try {
    
     resolve(go()) } catch (e) {
    
     reject(e) }
                })
            })
        }
        return go()
    }
}    

Os resultados de desempenho após a otimização são mostrados na figura abaixo (o efeito de animação é suave):
insira a descrição da imagem aqui
Comparamos o desempenho da função de corte de tempo antes e depois da otimização. Pode-se ver que a execução da CPU é dividida em seções antes da otimização , mas após a otimização, ele é dividido em seções. Sim, fora isso, não há muita diferença.
Mas vamos comparar os resultados de impressão dos dois:
antes da otimização:
insira a descrição da imagem aqui
depois da otimização:
insira a descrição da imagem aqui
daí podemos ver que o valor de i é bem diferente, 1037 antes da otimização e 6042162 depois da otimização, sendo que ambos são executados em 5 segundos O número de execuções JS dentro do período de tempo é refletido. É óbvio que apenas 1.037 execuções foram realizadas antes da otimização e 6.042.162 execuções podem ser alcançadas após a otimização. O número
insira a descrição da imagem aqui
de execuções ao mesmo tempo pode ser considerado como eficiência e a eficiência após a otimização é 5826 vezes maior que antes da otimização.

Observe que
depois de entender como a função do gerador otimiza a renderização da página por meio do fatiamento do tempo, devemos prestar atenção à transformação do gerador para tarefas longas (não é como um web worker executando diretamente um trecho de código, depende de como o indivíduo modifica o código original), a posição de yield é muito crítica e precisa ser colocada em um local de execução demorado. Por exemplo, a tarefa longa no exemplo acima é i++ ininterrupta em 5 segundos.

requestAnimationFrame

Este método é usado principalmente para resolver o congelamento da animação JS, portanto, a longa tarefa acima que causa o bloqueio da animação css do navegador não é aplicável.

Introdução

requestAnimationFrame é uma API de animação fornecida em HTML5, conhecida como rAF . Em aplicativos da Web, há muitas maneiras de obter efeitos de animação. Em Javascript , isso pode ser realizado pelo timer setTimeout ou setInterval. Em css3 , a transição e a animação podem ser usadas. Canvas em html5 Além disso, o html5 também fornece uma API dedicada a solicitar animação, ou seja, requestAnimationFrame , que, como o nome indica, solicita quadros de animação .

Conceitos relacionados

Taxa de atualização da tela
A taxa de atualização da tela pode ser visualizada nas "Configurações avançadas de exibição" do computador e geralmente é de 60 Hz. O "efeito de permanência visual" não sente alterações ou tremores, e o que você vê ainda é um contínuo Na verdade, o intervalo intermediário é de 16,7 ms (ou seja, 1000/60).

A página está visível
Quando a página é minimizada ou alternada para uma página de guia em segundo plano, a página fica invisível e o navegador aciona um evento visibilidadechange e define a propriedade document.hidden como true; ao alternar para o estado de exibição, a página é visível, e o mesmo Aciona um evento visibilidadechange, definindo a propriedade document.hidden como false.

Lista de funções de retorno de chamada de solicitação de quadro de animação
Cada Documento possui uma lista de função de retorno de chamada de solicitação de quadro de animação, que pode ser considerada como um conjunto composto de tuplas <handlerId, callback>. Entre eles, handlerId é um número inteiro que identifica exclusivamente a posição da tupla na lista; callback é uma função de retorno de chamada.

Por que usar requestAnimationFrame?

Porém, quando alguns efeitos de animação são realizados através do JS, por exemplo, com setTimeout, setTimeout é na verdade um efeito de animação formado pela atualização contínua de imagens ao longo de um intervalo de tempo. No entanto, setTimeout pode congelar em alguns modelos ou aplicativos complexos, o que geralmente é chamado de "perda de quadro". Isso ocorre porque setTimeout só pode definir um intervalo de tempo fixo, e telas e modelos diferentes terão resoluções diferentes, e a tarefa setTimeout é colocada em uma fila assíncrona, portanto, o tempo de execução real será posterior ao tempo definido. Esses motivos levam ao fenômeno travado da animação setTimeout.
Conhecendo as deficiências de setTimeout, o surgimento de rAF é natural. O tempo de execução da função de retorno de chamada rAF é determinado pelo sistema, ou seja, o sistema chamará ativamente a função de retorno de chamada em rAF antes de cada desenho. Se a frequência de desenho do sistema for 60 Hz, a função de retorno de chamada será executada uma vez a cada 16,7 ms. Se a frequência de desenho do sistema for 75 Hz, o intervalo de tempo será 1000/75 = 13,3 ms, o que garante que a função de retorno de chamada possa ser executada uma vez no meio de cada desenho e não haverá perda de quadro ou gagueira.
Além disso, requestAnimationFrame tem as duas vantagens a seguir:

  • Economia de energia da CPU : quando a animação implementada usando setTimeout, quando a página está oculta ou minimizada, setTimeout ainda executa a tarefa de animação em segundo plano. Como a página está invisível ou indisponível neste momento, não faz sentido atualizar a animação, o que é um desperdício de recursos da CPU. O requestAnimationFrame é completamente diferente. Quando o processamento da página não estiver ativado, a tarefa de atualização da tela da página também será suspensa pelo sistema, portanto o requestAnimationFrame que segue o sistema também irá parar de renderizar. Quando a página for ativada, a animação será comece a partir da última vez. Continue a executar onde está, economizando efetivamente a sobrecarga da CPU.
  • Limitação de função : Em eventos de alta frequência (redimensionar, rolar, etc.), para evitar várias execuções de função dentro de um intervalo de atualização, use requestAnimationFrame para garantir que a função seja executada apenas uma vez em cada intervalo de atualização, o que pode garantir um desempenho suave , e pode economizar melhor a sobrecarga da execução da função. Não faz sentido quando a função é executada várias vezes em um intervalo de atualização, porque a exibição é atualizada a cada 16,7 ms e vários desenhos não serão refletidos na tela.

usar

O exemplo simples mais comum na Internet (o rAF não é necessariamente usado para animação, também é bom para resolver alguns códigos JS executados com frequência):

var progress = 0;
//回调函数
function render() {
    
    
  progress += 1; //修改图像的位置
  if (progress < 100) {
    
    
    //在动画没有结束前,递归渲染
    window.requestAnimationFrame(render);
  }
}
//第一帧渲染
window.requestAnimationFrame(render);

Este método é assíncrono, a função passada é chamada antes da animação ser redesenhada.

// 传入一个callback函数,即动画函数;
// 返回值handlerId为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中位置。
handlerId = requestAnimationFrame(callback)

Processo de execução:
(1) Antes de tudo, é necessário julgar se a propriedade document.hidden é verdadeira, ou seja, será executada somente quando a página estiver visível; (2) O navegador limpa a rodada anterior de funções de
animação ;
(3) O valor handlerId retornado por este método será um retorno de chamada da função de animação, use <handlerId , callback> para inserir a lista de funções de retorno de chamada de solicitação de quadro de animação; (4) O navegador percorrerá a lista de funções de retorno de chamada de solicitação de quadro de animação
e executa as funções de animação correspondentes de acordo com o valor de handlerId.

exemplo específico

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    .move {
    
    
        position: absolute;
    }
</style>

<body>
    <div class="move" id="box">LHX-FLY</div>
</body>
<script>
    var box = document.getElementById('box')
    var flag = false
    var left = 0
    function render() {
    
    
        if (flag) {
    
    
            if (left >= 100) {
    
    
                flag = false
            }
            box.style.left = `${
      
      left++}px`
        } else {
    
    
            if (left <= 0) {
    
    
                flag = true
            }
            box.style.left = `${
      
      left--}px`
        }
        window.requestAnimationFrame(render)
    }
    render()
</ script>    

O efeito de execução do código é mostrado na figura:
insira a descrição da imagem aqui

Na verdade, a partir do código de exemplo do recurso, é muito perigoso chamar render em render, o método é recursivo, mas nenhum julgamento é adicionado. Normalmente, o navegador relatará um erro, mas não será executado com a janela .requestAnimationFrame callback (mas não é recomendado Escreva assim)
Como cancelar o callback em requestAnimationFrame e continuar executando?
Basta chamar o método cancelAnimationFrame:

 var rAFId = null;
 function render() {
    
    
    if (flag) {
    
    
        if (left >= 100) {
    
    
            flag = false
        }
        box.style.left = `${
      
      left++}px`
    } else {
    
    
        if (left <= 0) {
    
    
            flag = true
        }
        box.style.left = `${
      
      left--}px`
    }
    rAFId =  window.requestAnimationFrame(render)
    // 5 秒后取消动画效果
    setTimeout(function () {
    
    
        cancelAnimationFrame(rAFId)
    }, 5000)
}

O efeito é o mostrado na figura:
insira a descrição da imagem aqui
podemos ver que após 5 segundos, o efeito de animação é realmente cancelado.

Rebaixamento elegante

Porque requestAnimationFrame ainda tem problemas de compatibilidade e diferentes navegadores precisam carregar diferentes prefixos. Portanto, é necessário encapsular requestAnimationFrame por meio de degradação graciosa, dar prioridade ao uso de recursos avançados e, em seguida, reverter de acordo com a situação de diferentes navegadores até que apenas setTimeout possa ser usado.

;(function () {
    
    
	var lastTime = 0
	var vendors = ['ms', 'moz', 'webkit', 'o']
	for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
    
    
		window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
		window.cancelAnimationFrame =
			window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']
	}

	if (!window.requestAnimationFrame)
		window.requestAnimationFrame = function (callback, element) {
    
    
			var currTime = new Date().getTime()
			var timeToCall = Math.max(0, 16 - (currTime - lastTime))
			var id = window.setTimeout(function () {
    
    
				callback(currTime + timeToCall)
			}, timeToCall)
			lastTime = currTime + timeToCall
			return id
		}

	if (!window.cancelAnimationFrame)
		window.cancelAnimationFrame = function (id) {
    
    
			clearTimeout(id)
		}
})()

Artigo de referência:
Tecnologia de fatiamento de tempo (para resolver o congelamento de página causado por tarefas js longas)
uso de requestAnimationFrame

Acho que você gosta

Origin blog.csdn.net/weixin_43589827/article/details/122496049
Recomendado
Clasificación