Introdução
Neste artigo, vou ensinar como carregar e executar JavaScript no navegador.
Não, espera, volte! Sei que parece mundano e simples, mas lembre-se de que isso está acontecendo no navegador, onde o teoricamente simples se torna um problema legado. Conhecer essas peculiaridades permite que você escolha a maneira mais rápida e menos perturbadora de carregar scripts. Se você estiver com uma agenda apertada, pule para a referência rápida.
Para começar, veja como a especificação define as várias maneiras de fazer o download e a execução de um script:
Como todas as especificações do WHATWG, ela parece inicialmente o resultado de uma bomba de fragmentação em uma fábrica de scrabble, mas depois que você lê pela quinta vez e limpa o sangue dos seus olhos, ela é bem interessante:
Meu primeiro include de script
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Ah, a simplicidade maravilhosa. Aqui, o navegador faz o download dos dois scripts em paralelo e os executa assim que possível, mantendo a ordem. O arquivo "2.js" não será executado até que o arquivo "1.js" seja executado (ou falhe em fazer isso). O arquivo "1.js" não será executado até que o script ou a folha de estilos anterior seja executado etc.
Infelizmente, o navegador bloqueia a renderização da página enquanto isso acontece. Isso ocorre devido a APIs DOM da "primeira era da Web" que permitem que strings sejam anexadas ao conteúdo que o analisador está processando, como document.write
. Os navegadores mais recentes vão continuar verificando ou analisando o documento em segundo plano e acionar downloads para conteúdo externo que possa ser necessário (js, imagens, css etc.), mas a renderização ainda está bloqueada.
É por isso que os especialistas em performance recomendam colocar os elementos de script no final do documento, porque eles bloqueiam o menor conteúdo possível. Isso significa que o script não é visto pelo navegador até que todo o HTML seja transferido. Nesse ponto, ele começa a fazer o download de outros conteúdos, como CSS, imagens e iframes. Os navegadores modernos são inteligentes o suficiente para priorizar o JavaScript em vez de imagens, mas podemos fazer melhor.
Valeu, IE! Não, não estou sendo sarcástico.
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
A Microsoft reconheceu esses problemas de desempenho e introduziu o "adiamento" no Internet Explorer 4. Isso basicamente diz: "Prometo não injetar coisas no analisador usando coisas como document.write
. Se eu quebrar essa promessa, você pode me punir da forma que achar melhor". Esse atributo chegou ao HTML4 e apareceu em outros navegadores.
No exemplo acima, o navegador faz o download dos dois scripts em paralelo e os executa pouco antes de DOMContentLoaded
ser acionado, mantendo a ordem.
Como uma bomba de fragmentação em uma fábrica de ovelhas, "adiar" se tornou uma bagunça confusa. Entre os atributos "src" e "defer" e as tags de script em comparação com os scripts adicionados dinamicamente, temos seis padrões de adição de um script. Obviamente, os navegadores não concordam com a ordem em que devem ser executados. A Mozilla escreveu um ótimo artigo sobre o problema em 2009.
O WHATWG tornou o comportamento explícito, declarando que "adiar" não tem efeito nos scripts que foram adicionados dinamicamente ou não tinham "src". Caso contrário, os scripts adiados serão executados depois que o documento for analisado, na ordem em que foram adicionados.
Valeu, IE! (Ok, agora estou sendo sarcástico)
Ele dá e tira. Infelizmente, há um bug no IE4-9 que pode fazer com que os scripts sejam executados em uma ordem inesperada. Veja o que acontece:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
Supondo que haja um parágrafo na página, a ordem esperada dos registros é [1, 2, 3], embora no IE9 e versões anteriores, você receba [1, 3, 2]. Operações específicas do DOM fazem com que o IE interrompa a execução do script atual e execute outros scripts pendentes antes de continuar.
No entanto, mesmo em implementações sem bugs, como o IE10 e outros navegadores, a execução do script é adiada até que todo o documento seja transferido por download e analisado. Isso pode ser conveniente se você vai esperar por DOMContentLoaded
de qualquer maneira, mas se quiser ser muito agressivo com a performance, comece a adicionar listeners e inicialização mais cedo.
HTML5 para ajudar
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
O HTML5 nos deu um novo atributo, "async", que pressupõe que você não vai usar document.write
, mas não espera até que o documento seja analisado para ser executado. O navegador vai fazer o download dos dois scripts em paralelo e executá-los assim que possível.
Infelizmente, como eles vão ser executados o mais rápido possível, o "2.js" pode ser executado antes do "1.js". Isso é aceitável se eles forem independentes, talvez o "1.js" seja um script de rastreamento que não tem nada a ver com o "2.js". Mas se o "1.js" for uma cópia CDN do jQuery da qual o "2.js" depende, sua página vai ficar cheia de erros, como uma bomba de fragmentação em um… não sei… não tenho nada para isso.
Sei o que precisamos: uma biblioteca JavaScript.
O ideal é fazer o download de um conjunto de scripts imediatamente, sem bloquear a renderização, e executar o mais rápido possível na ordem em que foram adicionados. Infelizmente, o HTML não permite isso.
O problema foi resolvido pelo JavaScript em algumas versões. Algumas exigiam que você fizesse mudanças no JavaScript, envolvendo-o em um callback que a biblioteca chama na ordem correta (por exemplo, RequireJS). Outros usavam o XHR para fazer o download em paralelo e depois eval()
na ordem correta, o que não funcionava para scripts em outro domínio, a menos que eles tivessem um cabeçalho CORS e o navegador oferecesse suporte a ele. Alguns até usaram hacks supermágicos, como o LabJS.
Os hacks envolviam enganar o navegador para que ele fizesse o download do recurso de uma maneira que acionasse um evento na conclusão, mas evitasse a execução. No LabJS, o script seria adicionado com um tipo MIME incorreto, por exemplo, <script type="script/cache" src="...">
. Depois que todos os scripts fossem transferidos, eles seriam adicionados novamente com um tipo correto, esperando que o navegador os recebesse diretamente do cache e os executasse imediatamente, em ordem. Isso dependia de um comportamento conveniente, mas não especificado, e que quebrou quando os navegadores declarados pelo HTML5 não puderam fazer o download de scripts com um tipo não reconhecido. Vale a pena notar que o LabJS se adaptou a essas mudanças e agora usa uma combinação dos métodos deste artigo.
No entanto, os carregadores de script têm um problema de desempenho próprio. Você precisa esperar o download e a análise do JavaScript da biblioteca antes que qualquer um dos scripts gerenciados possa começar o download. Além disso, como vamos carregar o carregador de script? Como vamos carregar o script que informa ao carregador o que carregar? Quem vigia os vigilantes? Por que estou nu? Todas essas perguntas são difíceis.
Basicamente, se você precisa fazer o download de um arquivo de script extra antes mesmo de pensar em fazer o download de outros scripts, você já perdeu a batalha de performance.
O DOM ao resgate
A resposta está na especificação HTML5, embora esteja oculta na parte de baixo da seção de carregamento de script.
Vamos traduzir isso para "Humano":
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Os scripts que são criados e adicionados dinamicamente ao documento são assíncronos por padrão. Eles não bloqueiam a renderização e são executados assim que são transferidos por download, o que significa que podem aparecer na ordem errada. No entanto, podemos marcá-los explicitamente como não assíncronos:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Isso dá aos nossos scripts uma combinação de comportamento que não pode ser alcançada com HTML simples. Por serem explicitamente assíncronos, os scripts são adicionados a uma fila de execução, a mesma em que são adicionados no nosso primeiro exemplo de HTML simples. No entanto, por serem criados dinamicamente, eles são executados fora da análise de documentos. Assim, a renderização não é bloqueada durante o download. Não confunda o carregamento de script não assíncrono com o XHR síncrono, que nunca é uma boa ideia.
O script acima precisa ser incluído inline no cabeçalho das páginas, enfileirando os downloads de script o mais rápido possível sem interromper a renderização progressiva e executando o mais rápido possível na ordem especificada. O arquivo "2.js" pode ser baixado antes do "1.js", mas não será executado até que o arquivo "1.js" seja baixado e executado ou falhe. Eba! Execução ordenada, mas com download assíncrono.
O carregamento de scripts dessa maneira é compatível com tudo o que oferece suporte ao atributo assíncrono, com exceção do Safari 5.0 (o 5.1 é compatível). Além disso, todas as versões do Firefox e do Opera são compatíveis, já que as versões que não têm suporte ao atributo async executam scripts adicionados dinamicamente na ordem em que são adicionados ao documento.
Essa é a maneira mais rápida de carregar scripts, certo? Certo?
Se você estiver decidindo dinamicamente quais scripts carregar, sim. Caso contrário, talvez não. No exemplo acima, o navegador precisa analisar e executar o script para descobrir quais scripts serão transferidos por download. Isso oculta seus scripts dos scanners de pré-carregamento. Os navegadores usam esses scanners para descobrir recursos em páginas que você provavelmente vai visitar em seguida ou descobrir recursos de página enquanto o analisador está bloqueado por outro recurso.
Podemos adicionar a capacidade de descoberta novamente colocando o seguinte na parte de cima do documento:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Isso informa ao navegador que a página precisa de 1.js e 2.js. link[rel=subresource]
é semelhante a link[rel=prefetch]
, mas com semântica diferente. No momento, o recurso só é compatível com o Chrome, e você precisa declarar quais scripts serão carregados duas vezes, uma vez por elementos de link e outra no script.
Correção:eu disse originalmente que eles eram detectados pelo scanner de pré-carregamento, mas não são. Eles são detectados pelo analisador regular. No entanto, o scanner de pré-carregamento pode detectar esses scripts, mas ainda não faz isso. Já os scripts incluídos por código executável nunca podem ser pré-carregados. Agradeço a Yoav Weiss, que me corrigiu nos comentários.
Acho este artigo deprimente
A situação é deprimente e você deve se sentir deprimido. Não há uma maneira não repetitiva, mas declarativa, de fazer o download de scripts de forma rápida e assíncrona, controlando a ordem de execução. Com o HTTP2/SPDY, é possível reduzir a sobrecarga de solicitações a ponto de que a entrega de scripts em vários arquivos pequenos e individualmente em cache pode ser a maneira mais rápida. Imagine só:
<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>
Cada script de melhoria lida com um componente de página específico, mas requer funções de utilitário em dependencies.js. O ideal é fazer o download de tudo de forma assíncrona e executar os scripts de melhoria o mais rápido possível, em qualquer ordem, mas depois de dependencies.js. É o aprimoramento progressivo! Infelizmente, não há uma maneira declarativa de fazer isso, a menos que os scripts sejam modificados para rastrear o estado de carregamento de dependencies.js. Mesmo async=false não resolve esse problema, porque a execução de enhancement-10.js é bloqueada de 1 a 9. Na verdade, há apenas um navegador que permite isso sem hacks…
O IE tem uma ideia!
O IE carrega scripts de maneira diferente dos outros navegadores.
var script = document.createElement('script');
script.src = 'whatever.js';
O IE começa a fazer o download de "whatever.js" agora, outros navegadores não começam a fazer o download até que o script seja adicionado ao documento. O IE também tem um evento, "readystatechange", e uma propriedade, "readystate", que informam o progresso do carregamento. Isso é muito útil, porque permite controlar o carregamento e a execução de scripts de forma independente.
var script = document.createElement('script');
script.onreadystatechange = function() {
if (script.readyState == 'loaded') {
// Our script has download, but hasn't executed.
// It won't execute until we do:
document.body.appendChild(script);
}
};
script.src = 'whatever.js';
Podemos criar modelos de dependência complexos escolhendo quando adicionar scripts ao documento. O IE oferece suporte a esse modelo desde a versão 6. Muito interessante, mas ainda sofre com o mesmo problema de detecção de pré-carregador que async=false
.
Chega! Como devo carregar scripts?
Ok, ok. Se você quiser carregar scripts de uma maneira que não bloqueie a renderização, não envolva repetição e tenha um excelente suporte ao navegador, aqui está o que eu proponho:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Isso. No final do elemento body. Sim, ser um desenvolvedor da Web é muito parecido com ser o Rei Sísifo (bum! 100 pontos hipster para a referência da mitologia grega!). As limitações no HTML e nos navegadores nos impedem de fazer muito melhor.
Espero que os módulos JavaScript nos salvem, fornecendo uma maneira declarativa e não bloqueante de carregar scripts e controlar a ordem de execução, embora isso exija que os scripts sejam escritos como módulos.
Deve haver algo melhor que possamos usar agora?
Ok, como bônus, se você quiser melhorar muito a performance e não se importar com um pouco de complexidade e repetição, combine alguns dos truques acima.
Primeiro, adicionamos a declaração de subrecurso para os pré-carregadores:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Em seguida, inline na cabeça do documento, carregamos nossos scripts com JavaScript, usando async=false
, voltando ao carregamento de script baseado em readystate do IE, voltando a adiar.
var scripts = [
'1.js',
'2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we'll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
Depois de alguns truques e minificação, o tamanho é de 362 bytes + os URLs do script:
!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
"//other-domain.com/1.js",
"2.js"
])
Vale a pena usar os bytes extras em comparação com um script simples? Se você já usa JavaScript para carregar scripts condicionalmente, como a BBC, pode acionar esses downloads mais cedo. Caso contrário, talvez não, use o método simples de fim de corpo.
Ufa, agora sei por que a seção de carregamento de script do WHATWG é tão vasta. Preciso de uma bebida.
Referência rápida
Elementos de script simples
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
A especificação diz: faça o download em conjunto, execute em ordem após qualquer CSS pendente e bloqueie a renderização até a conclusão. Os navegadores dizem: Sim, senhor!
Adiar
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
A especificação diz: faça o download em conjunto e execute na ordem logo antes de DOMContentLoaded. Ignorar "adiar" em scripts sem "src". O IE < 10 diz: posso executar 2.js na metade da execução de 1.js. Não é divertido? Os navegadores em vermelho dizem: Não faço ideia do que se trata essa coisa de "adiar". Vou carregar os scripts como se não estivessem lá. Outros navegadores dizem: Ok, mas talvez eu não ignore "adiar" em scripts sem "src".
Assíncrona
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
A especificação diz: faça o download juntos e execute na ordem em que foram feitos. Os navegadores em vermelho dizem: O que é "assíncrono"? Vou carregar os scripts como se ele não estivesse lá. Outros navegadores dizem: Sim, ok.
Assíncrono falso
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
A especificação diz: faça o download de tudo junto e execute na ordem assim que todos os downloads forem concluídos. Firefox < 3.6, Opera:não tenho ideia do que é essa coisa “assíncrona”, mas acontece que eu executo os scripts adicionados por JS na ordem em que eles são adicionados. O Safari 5.0 diz: entendo "async", mas não entendo como definir como "false" com JS. Vamos executar seus scripts assim que eles forem recebidos, em qualquer ordem. O IE < 10 diz: Não sei o que é "async", mas há uma solução alternativa usando "onreadystatechange". Outros navegadores em vermelho dizem: Não entendo essa coisa de "async", vou executar seus scripts assim que eles chegarem, em qualquer ordem. O resto diz: sou seu amigo, vamos fazer isso corretamente.