Aguarde...

10 de janeiro de 2023

Por que não document.write()?

Por que não document.write()?

Se você já executou um teste do Lighthouse antes, há uma grande chance de ter visto a auditoria . Evite document.write():

Você também deve ter visto que há muito pouca explicação de por que document.write() é tão prejudicial. Bem, a resposta curta é:

De um ponto de vista puramente voltado para o desempenho, document.write()ele não é tão especial ou único. Na verdade, tudo o que ele faz é revelar potenciais comportamentos já presentes em qualquer script síncrono – a única diferença principal é que document.write()garante que esses comportamentos negativos se manifestarão, enquanto outros scripts síncronos podem fazer uso de otimizações alternativas para evitá-los.

NB Esta auditoria e, portanto, este artigo, lida apenas com o uso de injeção de script document.write()— não seu uso em geral. A entrada MDN para document.write() faz um bom trabalho em desencorajar seu uso.

O que torna os scripts lentos?

Há uma série de coisas que podem tornar os scripts regulares e síncronos 1 lentos:

  1. O JS síncrono pode bloquear a construção do DOM durante o download do arquivo.
    • A crença de que o JS síncrono bloqueia a construção do DOM só é verdadeira em determinados cenários.
  2. O Synchronous JS sempre bloqueia a construção do DOM enquanto o arquivo está em execução.
    • Ele é executado in-situ no ponto exato em que é definido, portanto, qualquer coisa definida após o script precisa esperar.
  3. Synchronous JS nunca bloqueia downloads de arquivos subseqüentes.
    • Isso é verdade há quase 15 anos no momento em que escrevo, mas ainda continua sendo um equívoco comum entre os desenvolvedores. Isso está intimamente relacionado ao primeiro ponto.

O pior cenário é um script que cai em (1) e (2), que é mais provável que afete os scripts definidos anteriormente em seu HTML. document.write(), no entanto, força os scripts em (1) e (2), independentemente de quando eles são definidos.

O scanner de pré-carregamento

A razão pela qual os scripts nunca bloqueiam downloads subseqüentes é por causa de algo chamado Preload Scanner . O Preload Scanner é um analisador secundário, inerte, apenas para download, responsável por executar o HTML e solicitar de forma assíncrona quaisquer sub-recursos disponíveis que possa encontrar, principalmente qualquer coisa contida srcou hrefatributos, incluindo imagens, scripts, folhas de estilo, etc. Como resultado, os arquivos obtidos por meio do Preload Scanner são paralelizados e podem ser baixados de forma assíncrona junto com outros recursos (potencialmente síncronos).

O Preload Scanner é desacoplado do analisador primário, que é responsável por construir o DOM, o CSSOM, executar scripts etc. Isso significa que a grande maioria dos arquivos que buscamos é feita de forma assíncrona e sem bloqueio, incluindo scripts síncronos. É por isso que nem todos os scripts de bloqueio são bloqueados durante a fase de download – eles podem ter sido obtidos pelo Preload Scanner antes de serem realmente necessários, portanto, de maneira não-bloqueante.

O Preload Scanner e o analisador primário começam a processar o HTML mais ou menos ao mesmo tempo, então o Preload Scanner realmente não tem muita vantagem. É por isso que os primeiros scripts têm mais probabilidade de bloquear a construção do DOM durante a fase de download do que os scripts tardios: o analisador principal tem mais probabilidade de encontrar o <script src>elemento relevante durante o download do arquivo se o <script src>elemento estiver no início do HTML. Late (por exemplo, at- </body>) s síncronos <script src>são mais prováveis ​​de serem buscados pelo Preload Scanner enquanto o analisador principal ainda está desligado fazendo o trabalho no início da página.

Simplificando, os scripts definidos anteriormente na página têm maior probabilidade de serem bloqueados no download do que os posteriores; é mais provável que os scripts posteriores tenham sido buscados de forma preventiva e assíncrona pelo Preload Scanner.

document.write()Oculta arquivos do verificador de pré-carregamento

Como o Preload Scanner lida com tokenizeable srchrefatributos, qualquer coisa oculta em JavaScript é invisível para ele:

<script>
  document.write('<script src=file.js><\/script>')
</script>

Esta não é uma referência a um script; esta é uma string em JS. Isso significa que o navegador não pode solicitar esse arquivo até que ele realmente execute o <script>bloco que o insere, o que é just-in-time (e tarde demais).

document.write()força os scripts a bloquear a construção do DOM durante o download, ocultando-os do Preload Scanner.

E os trechos assíncronos?

Trechos assíncronos como o abaixo sofrem o mesmo destino:

<script>
  var script = document.createElement('script');
  script.src = 'file.js';
  document.head.appendChild(script);
</script>

Novamente, file.jsnão é um caminho de arquivo — é uma string! Não é até que o navegador execute esse script que ele coloca um srcatributo no DOM e pode solicitá-lo. A principal diferença aqui, porém, é que os scripts injetados dessa maneira são assíncronos por padrão. Apesar de estar oculto do Preload Scanner, o impacto é insignificante porque o arquivo é implicitamente assíncrono de qualquer maneira.

Dito isso, os snippets assíncronos ainda são um antipadrão — não os use.

document.write()Executa sincronizadamente

document.write()é usado quase exclusivamente para carregar condicionalmente um script síncrono. Se você só precisa de um script de bloqueio , use um elemento simples <script src>:

<script src=file.js></script>

Se você precisasse carregar condicionalmente um script assíncrono , adicionaria alguma lógica ifelseao seu snippet assíncrono.

<script>

  if (condition) {
    var script = document.createElement('script');
    script.src = 'file.js';
    document.head.appendChild(script);
  }

</script>

Se você precisa carregar condicionalmente um script síncrono , você está meio travado…

Os scripts injetados com, por exemplo, appendChildsão, de acordo com a especificação, assíncronos. Se você precisar injetar um arquivo síncrono, uma das únicas opções diretas é document.write():

<script>

  if (condition) {
    document.write('<script src=file.js><\/script>')
  }

</script>

Isso garante uma execução síncrona, que é o que queremos, mas também garante uma busca síncrona, porque ela fica oculta do Preload Scanner, que é o que não queremos.

document.write()força os scripts a bloquear a construção do DOM durante sua execução por serem síncronos por padrão.

É tudo ruim?

A localização do document.write()em questão faz uma grande diferença.

Como o Preload Scanner funciona com mais eficiência quando está lidando com sub-recursos mais adiante na página, o document.write()início no HTML é menos prejudicial.

Cedodocument.write()

<head>

  ...

  <script>
    document.write('<script src=https://slowfil.es/file?type=js&delay=1000><\/script>')
  </script>

  <link rel=stylesheet href=https://slowfil.es/file?type=css&delay=1000>

  ...

</head>

Se você colocar um document.write()como a primeira coisa em seu <head>, ele se comportará exatamente da mesma forma que um regular – o <script src>Preload Scanner não teria muita vantagem de qualquer maneira, então já perdemos a chance de um busca assíncrona:

document.write()como a primeira coisa no arquivo <head>. FCP está em 2.778s.

Acima, vemos que o navegador conseguiu paralelizar as solicitações: o analisador primário executou e injetou o document.write(), enquanto o Preload Scanner buscou o CSS.

Devido à prioridade mais alta do CSS , ele sempre será solicitado antes do JS de alta prioridade, independentemente de onde cada um for definido.

Se substituirmos o document.write()por um simple <script src>, veremos exatamente o mesmo comportamento, ou seja, neste caso específico, document.write()não é mais prejudicial do que um script síncrono regular:

<head>

  ...

  <script src=https://slowfil.es/file?type=js&delay=1000></script>

  <link rel=stylesheet href=https://slowfil.es/file?type=css&delay=1000>

  ...

</head>

Isso produz uma cascata idêntica:

Usando um síncrono <script src>em vez de document.write(). FCP está em 2.797s.

Como era improvável que o Preload Scanner encontrasse qualquer uma das variantes, não notamos nenhuma degradação real.

Tarde document.write()

<head>

  ...

  <link rel=stylesheet href=https://slowfil.es/file?type=css&delay=1000>

  <script>
    document.write('<script src=https://slowfil.es/file?type=js&delay=1000><\/script>')
  </script>

  ...

</head>

Como o JS pode gravar/ler de/para o CSSOM, todos os navegadores interromperão a execução de qualquer JS síncrono se houver algum CSS anterior pendente. Na verdade, o CSS bloqueia o JS e, neste exemplo, serve para ocultar o document.write()do Preload Scanner.

Assim, document.write()mais adiante na página torna-se mais grave. Ocultar um arquivo do Preload Scanner – e apenas exibi-lo no navegador no momento exato em que precisamos dele – fará com que toda a sua busca seja uma ação de bloqueio. E, como o document.write()arquivo agora está sendo obtido pelo analisador principal (ou seja, o thread principal), o navegador não pode concluir nenhum outro trabalho enquanto o arquivo estiver a caminho. Bloqueio em cima de bloqueio.

Assim que ocultamos o arquivo de script do Preload Scanner, notamos um comportamento drasticamente diferente. Simplesmente trocando o document.write()e ao rel=stylesheetredor, obtemos uma experiência muito, muito mais lenta:

document.write()tarde no <head>. FCP está em 4.073s.

Agora que ocultamos o script do Preload Scanner, perdemos toda a paralelização e incorremos em uma penalidade muito maior.

Fica pior…

O motivo pelo qual estou escrevendo este post é que tenho um cliente no momento que está usando document.write()no final do <head>. Como sabemos agora, isso empurra tanto a busca quanto a execução no thread principal. Como os navegadores são de thread único, isso significa que não apenas estamos incorrendo em atrasos de rede (graças a uma busca síncrona), mas também deixando o navegador incapaz de trabalhar em qualquer outra coisa durante todo o download do script!

O thread principal fica completamente silencioso durante a busca do arquivo injetado. Isso não acontece quando os arquivos são buscados no Preload Scanner.

Evitar document.write()

Além de exibir um comportamento imprevisível e com erros, conforme enfatizado nos artigos do MDN e do Google, document.write()é lento. Ele garante uma busca de bloqueio e uma execução de bloqueio, o que atrasa o analisador por muito mais tempo do que o necessário. Embora não introduza nenhum problema de desempenho novo ou exclusivo em si, apenas força o pior de todos os mundos.

Postado em Blog
Escreva um comentário