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:
- 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.
- A crença de que
- 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.
- 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 src
ou href
atributos, 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 src
e href
atributos, 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.js
não é um caminho de arquivo — é uma string! Não é até que o navegador execute esse script que ele coloca um src
atributo 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 if
/ else
ao 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, appendChild
sã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:
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:
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=stylesheet
redor, obtemos uma experiência muito, muito mais lenta:
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!
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.