Utilizando Módulos JavaScript em Produção: Situação Atual. Parte 1

Há dois anos, escrevi sobre uma técnica que agora é comumente chamada de padrão módulo / nomódulo. Seu aplicativo permite escrever código JavaScript usando os recursos do ES2015 + e, em seguida, usar bundlers e transpilers para criar duas versões da base de código. Um deles contém sintaxe moderna (é carregado usando uma estrutura como <script type="module"> e o segundo é a sintaxe ES5 (carregado com <script nomodule> ). O padrão module / nomodule permite enviar para navegadores que suportam módulos, muito menos código do que os navegadores que não suportam esse recurso, agora esse padrão é suportado pela maioria das estruturas da Web e ferramentas de linha de comando.



Anteriormente, mesmo considerando a capacidade de enviar código JavaScript moderno para produção, e mesmo que a maioria dos navegadores suportasse módulos, eu recomendei a coleta de código em pacotes configuráveis.

Porque Principalmente porque tive a sensação de que o carregamento dos módulos no navegador era lento. Embora protocolos recentes, como o HTTP / 2, teoricamente suportassem o carregamento eficiente de vários arquivos, todos os estudos de desempenho da época concluíram que o uso de bundlers ainda é mais eficiente do que o uso de módulos.

Mas deve-se admitir que esses estudos estavam incompletos. Os casos de teste usando os módulos estudados consistiram em arquivos de código-fonte não otimizados e não minimizados que foram implementados na produção. Não houve comparações do pacote otimizado com os módulos com o script clássico otimizado.

No entanto, para ser sincero, não havia uma maneira ideal de implantar os módulos naquele momento. Mas agora, graças a algumas melhorias modernas nas tecnologias de empacotador, é possível implantar o código de produção na forma de módulos ES2015 usando comandos de importação estáticos e dinâmicos e, ao mesmo tempo, obter um desempenho superior ao obtido com as opções disponíveis, nas quais módulos não são usados.

Note-se que no site em que o material original é publicado, a primeira parte da tradução que publicamos hoje, os módulos são utilizados na produção há vários meses.

Equívocos sobre módulos


Muitas pessoas com quem conversei rejeitam completamente os módulos, nem mesmo os consideram uma das opções para aplicativos de produção em larga escala. Muitos deles citam o próprio estudo que eu já mencionei. Nomeadamente, a parte dele, que afirma que os módulos não devem ser usados ​​na produção, a menos que seja uma questão de “pequenas aplicações web, que incluem menos de 100 módulos que diferem em uma árvore de dependência relativamente“ pequena ”(ou seja, - aquele cuja profundidade não exceda 5 níveis). ”

Se você já examinou o diretório node_modules de qualquer um de seus projetos, provavelmente sabe que mesmo um aplicativo pequeno pode facilmente ter mais de 100 módulos de dependência. Quero oferecer uma olhada em quantos módulos estão disponíveis em alguns dos pacotes npm mais populares.
Pacote
Número de módulos
date-fns
729
lodash-es
643
rxjs
226

É aqui que o principal equívoco referente aos módulos está enraizado. Os programadores acreditam que, quando se trata de usar módulos na produção, eles têm apenas duas opções. A primeira é implantar todo o código-fonte em seu formato existente (incluindo o diretório node_modules ). O segundo é não usar módulos.

No entanto, se você examinar atentamente as recomendações do estudo citado acima, descobrirá que não há nada a dizer que o carregamento de módulos é mais lento do que o carregamento de scripts regulares. Não diz que os módulos não devem ser utilizados. Ele apenas fala sobre o fato de que se alguém implantar centenas de arquivos de módulo não infectados na produção, o Chrome não poderá carregá-los tão rapidamente quanto um único pacote compactado. Como resultado, o estudo recomenda continuar usando bundlers, compiladores e minificadores.

Mas você sabe o que? O fato é que você pode usar tudo isso e usar módulos na produção.

De fato, os módulos são um formato no qual devemos nos esforçar para converter, já que os navegadores já sabem como carregar módulos (e os navegadores que não podem fazer isso podem carregar uma versão sobressalente do código usando o mecanismo de nomodule). Se você observar o código que os empacotadores mais populares geram, encontrará muitos fragmentos de modelo cujo objetivo é apenas carregar dinamicamente outro código e gerenciar dependências. Mas tudo isso não será necessário se apenas usarmos os módulos e expressões import e export .

Felizmente, pelo menos um dos empacotadores modernos populares ( Rollup ) suporta módulos na forma de dados de saída . Isso significa que você pode processar o código com um bundler e implantar módulos na produção (sem usar fragmentos de modelo para carregar o código). E, como o Rollup possui uma excelente implementação do algoritmo de agitação de árvore (o melhor que já vi nos empacotadores), a criação de programas na forma de módulos usando o Rollup permite obter um código menor que o tamanho do mesmo código obtido ao aplicar outros mecanismos disponíveis hoje.

Note-se que eles planejam adicionar suporte para módulos na próxima versão do Parcel. O Webpack ainda não suporta módulos como um formato de saída, mas aqui está - discussões que focam nessa questão.

Outro equívoco em relação aos módulos é que algumas pessoas acreditam que os módulos só podem ser usados ​​se 100% das dependências do projeto usarem módulos. Infelizmente (acho que é um grande arrependimento), a maioria dos pacotes npm ainda está sendo preparada para publicação usando o formato CommonJS (alguns módulos, mesmo aqueles escritos usando os recursos do ES2015, são traduzidos para o formato CommonJS antes de serem publicados no npm)!

Aqui, novamente, quero observar que o Rollup possui um plug-in ( rollup-plugin-commonjs ) que pega o código-fonte de entrada gravado usando o CommonJS e o converte no código ES2015. Definitivamente, seria melhor se o formato de dependência usado desde o início usasse o formato de módulo ES2015. Porém, se algumas dependências não forem assim, isso não impedirá que você implante projetos usando módulos em produção.

Nas partes seguintes deste artigo, mostrarei como coleciono projetos em pacotes configuráveis ​​que usam módulos (incluindo o uso de importações dinâmicas e separação de código), discutirei por que essas soluções geralmente são mais produtivas que os scripts clássicos e mostramos como elas funcionam. com navegadores que não suportam módulos.

Estratégia de criação de código ideal


O código de construção da produção é sempre uma tentativa de equilibrar os prós e os contras de várias soluções. Por um lado, o desenvolvedor deseja que seu código seja carregado e executado o mais rápido possível. Por outro lado, ele não deseja baixar o código que não será usado pelos usuários do projeto.

Além disso, os desenvolvedores precisam ter certeza de que seu código é mais adequado para armazenamento em cache. O grande problema do pacote de códigos é que qualquer alteração no código, mesmo uma linha alterada, leva à invalidação do cache de todo o pacote. Se você implantar um aplicativo que consiste em milhares de pequenos módulos (apresentados exatamente na forma em que estão presentes no código-fonte), poderá fazer pequenas alterações no código com segurança e ao mesmo tempo saber que a maior parte do código do aplicativo será armazenada em cache . Mas, como eu já disse, essa abordagem ao desenvolvimento provavelmente pode significar que o carregamento do código na primeira visita ao recurso pode demorar mais do que quando se usa abordagens mais tradicionais.

Como resultado, enfrentamos uma tarefa difícil, que é encontrar a abordagem correta para dividir os pacotes em partes. Precisamos encontrar o equilíbrio certo entre a velocidade de carregamento dos materiais e o armazenamento em cache a longo prazo.

A maioria dos empacotadores, por padrão, usa técnicas de divisão de código com base em comandos de importação dinâmica. Mas eu diria que dividir o código apenas com foco na importação dinâmica não permite dividi-lo em fragmentos suficientemente pequenos. Isso é especialmente verdadeiro para sites com muitos usuários recorrentes (ou seja, em situações em que o cache é importante).

Eu acredito que o código deve ser dividido em fragmentos tão pequenos quanto possível. Vale a pena reduzir o tamanho dos fragmentos até o número deles aumentar tanto que isso afetará a velocidade de download do projeto. E embora eu recomendo definitivamente que todos realizem sua própria análise da situação, se você acredita nos cálculos aproximados feitos no estudo que mencionei, ao carregar menos de 100 módulos, não há uma desaceleração perceptível no carregamento. Um estudo separado sobre o desempenho do HTTP / 2 não revelou uma desaceleração perceptível no projeto ao baixar menos de 50 arquivos. No entanto, testamos apenas as opções em que o número de arquivos era 1, 6, 50 e 1000. Como resultado, provavelmente 100 arquivos são o valor que você pode navegar facilmente sem medo de perder a velocidade de download.

Então, qual é a melhor maneira de dividir agressivamente, mas não muito agressivamente, o código em partes? Além da separação de código com base nos comandos de importação dinâmica, aconselho que você dê uma olhada mais de perto na separação de código pelos pacotes npm. Com essa abordagem, o que é importado para o projeto da pasta node_modules cai em um fragmento separado do código finalizado com base no nome do pacote.

Separação de Pacotes


Eu disse acima que alguns dos recursos modernos dos empacotadores tornam possível organizar um esquema de alto desempenho para a implantação de projetos baseados em módulos. O que eu estava falando é representado por dois novos recursos de Rollup. O primeiro é a separação automática de código através dos comandos import() dinâmicos (adicionados na v1.0.0 ). A segunda opção é a separação manual de código realizada pelo programa com base na opção manualChunks (adicionada na v1.11.0 ).

Graças a esses dois recursos, agora é muito fácil configurar o processo de compilação, no qual o código é dividido no nível do pacote.

Aqui está um exemplo de configuração que usa a opção manualChunks , graças à qual cada módulo importado de node_modules cai em um fragmento de código separado cujo nome corresponde ao nome do pacote (tecnicamente, o nome do diretório do pacote na pasta node_modules ):

 export default {  input: {    main: 'src/main.mjs',  },  output: {    dir: 'build',    format: 'esm',    entryFileNames: '[name].[hash].mjs',  },  manualChunks(id) {    if (id.includes('node_modules')) {      //   ,    `node_modules`.      //   - ,       .      const dirs = id.split(path.sep);      return dirs[dirs.lastIndexOf('node_modules') + 1];    }  }, } 

A opção manualChunk aceita uma função que aceita, como argumento único, o caminho para o arquivo do módulo. Esta função pode retornar um nome de string. O que ele retornará apontará para um fragmento da montagem ao qual o módulo atual deve ser adicionado. Se a função não retornar nada, o módulo será adicionado ao fragmento padrão.

Considere um aplicativo que importe os cloneDeep() , debounce() e find() do pacote lodash-es . Se você aplicar a configuração acima ao criar este aplicativo, cada um desses módulos (assim como cada módulo lodash importado por esses módulos) será colocado em um único arquivo de saída com um nome como npm.lodash-es.XXXX.mjs (aqui XXXX é exclusivo hash do arquivo de módulo no fragmento lodash-es ).

No final do arquivo, você verá uma expressão de exportação como a seguinte. Observe que esta expressão contém apenas comandos de exportação para os módulos adicionados ao fragmento, e nem todos os módulos lodash .

 export {cloneDeep, debounce, find}; 

Então, se o código em qualquer um dos outros fragmentos usar esses módulos lodash (talvez apenas o método debounce() ), nesses fragmentos, em sua parte superior, haverá uma expressão de importação com a seguinte aparência:

 import {debounce} from './npm.lodash.XXXX.mjs'; 

Esperamos que este exemplo tenha esclarecido a questão de como a separação manual de códigos funciona no pacote cumulativo. Além disso, acho que os resultados da separação de código usando as expressões de import e export são muito mais fáceis de ler e entender do que o código de fragmentos, cuja formação utilizou mecanismos não padrão que são usados ​​apenas em um determinado empacotador.

Por exemplo, é muito difícil descobrir o que está acontecendo no próximo arquivo. Esta é a saída de um dos meus projetos antigos que usavam o webpack para dividir o código. Quase tudo neste código não é necessário em navegadores que suportam módulos.

 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([["import1"],{ /***/ "tLzr": /*!*********************************!*\  !*** ./app/scripts/import-1.js ***!  \*********************************/ /*! exports provided: import1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "import1", function() { return import1; }); /* harmony import */ var _dep_1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./dep-1 */ "6xPP"); const import1 = "imported: " + _dep_1__WEBPACK_IMPORTED_MODULE_0__["dep1"]; /***/ }) }]); 

E se houver centenas de dependências npm?


Como eu já disse, acredito que a separação no nível do código no nível do pacote geralmente permite ao desenvolvedor entrar em uma posição favorável quando a separação do código é realizada de forma agressiva, mas não muito agressiva.

Obviamente, se o seu aplicativo importar módulos de centenas de pacotes npm diferentes, você ainda poderá estar em uma situação em que o navegador não poderá carregá-los com eficiência.

No entanto, se você realmente possui muitas dependências npm, não deve abandonar completamente essa estratégia por enquanto. Lembre-se de que você provavelmente não baixará todas as dependências npm em todas as páginas. Portanto, é importante descobrir quantas dependências realmente carregam.

No entanto, estou certo de que existem alguns aplicativos reais que possuem tantas dependências npm que essas dependências simplesmente não podem ser representadas como fragmentos separados. Se o seu projeto é exatamente isso - eu recomendaria que você procure uma maneira de agrupar pacotes onde o código no qual com alta probabilidade possa mudar ao mesmo tempo (como react e react-dom ), pois a react-dom cache de fragmentos com esses pacotes também será executada ao mesmo tempo Posteriormente, mostrarei um exemplo no qual todas as dependências do React estão agrupadas no mesmo fragmento .

Para continuar ...

Caros leitores! Como você aborda o problema da separação de código em seus projetos?

Source: https://habr.com/ru/post/pt466537/


All Articles