JS é uma linguagem de programação fantástica, que te dá a possibilidade de fazer inúmeras coisas — desde um gráfico bem ilustrado, até modelos 3Ds incríveis.
Mas o JavaScript não se limita a gráficos bonitos ou interfaces coloridas.
Precisa de uma API? Ótimo, o Node.js pode te atender.
Quer uma aplicação performática? Builders modernos com SSR e SSG podem resolver seus problemas (e talvez até criar outros no caminho) com velocidade.
Aplicações mobile? WebViews criadas com frameworks JS vão te salvar.
Quer criar seu próprio jogo? Dá uma olhada no Phaser.
A lista é extensa: bots, pacotes NPM, Machine Learning... JS é praticamente o gênio do Aladdin esperando para sair da lâmpada: "Você nunca teve um amigo como eu."
Mas, ainda assim, temos os críticos, não precisa pesquisar muito para encontrar alguém reclamando do JS no Reddit, no Stack Overflow ou em algum fórum de programação.
Os motivos variam desde opiniões pessoais até coisas que, de fato, são difíceis de defender no JavaScript...principalmente quando falamos de coerções de tipo e comparações inesperadas.
Além disso, há frustrações práticas: ter que alinhar versões dos pacotes NPM no package.json
para que tudo volte a funcionar depois de um erro inesperado é algo comum. Problemas com dependências quebradas por atualizações são reais, e podem virar uma dor de cabeça.
E você ainda pode ter mais azar: cair num problema de incompatibilidade entre CJS e ESM — o motivo que originou este post:
É... e então, de repente, aquele seu amigo gênio começa a virar um pesadelo.
Cada versão de cada pacote pode se tornar seu inimigo.
E o pior: qual pacote é o culpado?
Você só vai descobrir trocando as versões, deletando o node_modules
, dando npm install
, rodando npm start
e torcendo.
Cada tentativa é um teste de fé, corrigir o pacote X pode quebrar o Y e
a tarefa que parecia durar 2 horinhas vira um loop eterno de frustração. Parece raro… mas acontece muito.
Queria te dizer que existe uma bala de prata pra resolver isso — mas não tem.
Ninguém vai vir te salvar. 🤯
O que dá pra fazer é entender os mecanismos, a história, e os "porquês" por trás dessas decisões no JavaScript.
Conhecer a linguagem a fundo vai te dar mais controle, vai fazer você parar de suar frio quando vir o infame diretório node_modules
nos logs de erro.
A ORIGEM DO PROBLEMA ENTRE CJS E ESM
Se você voltar um pouco no tempo e na história do JS, vai notar que o motor V8 (o interpretador do JS do Chrome) era executado apenas no lado do cliente.
O Node ainda não existia, e não tínhamos JavaScript no backend.
O JS no lado do cliente não era nem de longe "parrudo" e complexo como é hoje.
Módulos? Isso não existia. Empacotadores como Webpack? Só daqui a alguns anos. Frameworks? Muito futurista.
Naquela época, JavaScript sequer tinha noção de modularidade. Não existia import
, nem export
. Tudo que você escrevia, você torcia pra não colidir com mais nada.
O que tínhamos era um arquivo index.html que carregava alguns JS hospedado em algum servidor, e quando esse arquivo era consumido, o navegador apenas interpretava os JS com o V8 — e com isso, renderizava a aplicação na tela.
Quem reinava nessa época era o nosso saudoso jQuery, que ainda está presente em aproximadamente 73% de todos os sites da web!!
Mas aí vem a pergunta: como as coisas eram feitas no passado? se não tínhamos módulos? Todo o código da aplicação era inserido em um único JS?
Quase isso.
Nesse período, tínhamos o famoso "script soup": o arquivo index.html
era encarregado de carregar todos os JS da aplicação — seja via CDN, seja arquivos próprios:
Parecia simples e até fácil de controlar.
Mas aí começa o caos: todos os arquivos JS carregados dessa forma são executados no mesmo escopo global.
O que isso significa? Que funções e variáveis declaradas em um arquivo podem ser sobrescritas por outro sem nem te avisar.
E mais: os scripts são executados na ordem em que aparecem no HTML.
Ou seja, se você tentar usar uma função antes de ela ser carregada, pode dar erro — mesmo que ela exista em outro arquivo.
Vamos a um exemplo simples:
script1.js
const dogsName = 'Zelda';
function sayHiToDog() {
console.log("Hi " + dogsName);
}
script2.js
const dogsName = "McIntosh";
Se esses dois scripts forem incluídos no mesmo HTML, você vai se deparar com esse erro no console:
Uncaught SyntaxError: Identifier 'dogsName' has already been declared (at script2.js:1:1)
Por quê?
Porque o conteúdo dos arquivos JS é carregado como se tudo estivesse em um mesmo arquivo gigante.
Todos os valores, funções e variáveis vazam para o escopo global. Isso abre espaço pra conflitos, sobrescrições e bugs que só aparecem no runtime.
Longe do caos da web
Um pouco distante do cenário mencionado acima, em 2009 tivemos o lançamento do Node.js (para felicidade de alguns e tristeza de outros): tínhamos agora JS no backend!
Pra lidar com a bagunça que era a gestão de arquivos no frontend — cheio de scripts globais brigando por espaço — o Node adotou um modelo de modularização inspirado em uma proposta da comunidade chamada commonJS.
Nasce uma solução no backend para iluminar o frontend
O commonJS era uma maneira mais elegante de lidar com importação de trechos de código para os arquivos, por que? porque ele tratava os arquivos/dependências como módulos!
. Com o commonJS, o JavaScript finalmente ganhava uma forma real de modularização prática e isolada, sem depender de escopo global.
Se você já mexeu em alguma aplicação nodeJS com certeza já viu essas palavrinhas soltas por ai: require
e module.exports
, e são justamente elas que faziam a mágica acontecer. O require
importava o que outros arquivos exportavam como módulo e o module.exports
exportava o conteúdo que você queria dos seus scripts.
const dogsName = "McIntosh";
module.exports = {dogsName}
E para consumir isso bastava você chamar o require
const {dogsName} = require('./script1')
console.log(dogsName) //McIntosh
O commonJS no front
Com o nodeJS ganhando cada vez mais espaço, os desenvolvedores começaram a se dedicar na criação de pacote modularizados(npm
) para suas aplicações, pacotes esses com funções reutilizáveis que poderiam ser adicionados em qualquer aplicação, e as libs mais antigas ao commonJS (lodash e o axios por exemplo) começaram a seguir esse caminho também e se modularizaram utilizando essa mecânica, que revolucionou como o JS lidava com a gestão do código. Mas ai temos um problema, o navegador não entendia essa sintaxe de require e module.exports do nodeJS ☠, se você fizer um teste rápido no seu navegador colocando isso:
const dogsName = require('dogsLib');
O navegador não vai reclamar que dogsLib
não existe (e não existe mesmo) ele vai é reclamar que require
não existe! por conta da execução quebrar antes da tentativa da importação.
VM361:1 Uncaught ReferenceError: require is not defined
E como resolvemos isso ? como vamos trazer um pedacinho do nodeJS para o navegador ? é aí que surgem os bundlers — ferramentas que ajudam a trazer essa modularização para o ambiente do navegador.
Sobre os Bundlers
Quando estamos no nosso processo de desenvolvimento de aplicações no front, raramente nos atentamos aos bundlers (webpack, browserify, esbuild...) a menos quando temos algum erro que cite que o bundler quebrou no processo de compilação. Mas para que serve os bundlers e como eles solucionaram esse problema de trazer a modularização para o front?
Bom, entre as inúmeras funções que um bundler tem uma delas é a de deixar o código "entendível" ao navegador, quando adicionamos um bundler na aplicação o fluxo deixa de ser:
Seu JS -> Navegador
para:
Seu JS -> bundler -> Navegador
O navegador não sabe o que é a sintaxe de modularização do nodeJS e o bundler sabe disso, então nesse caso o bundler vai agir também como um tradutor
para o navegador, ele vai compilar o seu código JS em algo que o navegador não tenha nenhuma dificuldade de entender tentando manter ao máximo a fidelidade do que o seu código está fazendo e como ele está lidando com a gestão dos módulos. E com a adição dos bundlers no front voa lá, temos modularização em aplicações web também.
Nem tudo são flores
Todo começo de relacionamento é um mar de flores, mas nem todos eles continuam assim com o passar do tempo, e foi justamente isso que aconteceu na relação navegador e CJS. O CJS revolucionou e não foi pouco não a maneira como os desenvolvedores web olhavam para a estrutura do seu código, mas mesmo assim o commonJS
não tinha nascido para o front, ele foi "empurrado" para lá e com isso os problemas que não notávamos no backend começaram a se apresentar no frontend. Problemas como:
- performance: A execução síncrona do CJS pode ser um gargalo no cliente, onde o carregamento não pode bloquear a renderização da página. Somado a isso temos problemas que impossibilitam muito uma tentativa de tree-shaking do que realmente está sendo usado do módulo e do que não está.
- inconsistência: apesar existir uma certa coerência, especialmente em épocas anteriores, diferentes bundlers implementavam o suporte ao CJS de maneiras ligeiramente distintas, gerando uma certa inconsistência no momento de build (o mesmo arquivo tinha builds diferentes no webpack e em outro bundler).
- sem suporte: a comunidade entendeu a dor de não dar suporte para os módulos e começou a trabalhar em uma maneira diferente de gerir isso, criando o ESM, ao invés de dar suporte ao CJS.
Conhecemos o ESM
O escmaScript 2015 (ES6) foi considerado por alguns uma das versões que mais revolucionaram o JS, e não estou nem falando da criação de features modernas que tiraram aquela aparência de "linguagem remendada" do JS como: variáveis que respeitam o escopo que são declaradas (let
e const
), arrow functions ou classes, estamos falando dos ESModules
. Se o CJS era a solução elegante ao carregamento global de scripts, o ESM era a solução elegante ao CJS. O CJS era popular e difundido na comunidade web (o que vai ser um problemão) e o desafio do ESM era provar o seu valor cobrindo as brechas deixadas pelo CJS no web.
ESM vs CJS na Web — Comparativo de Benefícios
Característica | ESM (ES Modules) | CJS (CommonJS) |
---|---|---|
Suporte nativo em navegadores | ✅ Sim | ❌ Não (precisa de bundler) |
Carregamento assíncrono | ✅ Sim | ❌ Não, é síncrono |
Tree-shaking (remoção de código) | ✅ Suportado pelos bundlers modernos | ❌ Ineficiente, exige soluções manuais |
Importação dinâmica (import() ) |
✅ Suporte nativo | ✅ Suporte via require()
|
Análise estática de dependências | ✅ Sim, desde o parse inicial | ❌ Não, requer execução |
Paralelização de carregamento | ✅ Sim | ❌ Não |
Execução condicional de módulos | ❌ Não (estrutura rígida) | ✅ Sim (pode usar lógica com require ) |
Compatibilidade com o Node.js | ✅ Sim (desde Node 12+ com type: module ) |
✅ Sim, nativamente |
Padrão moderno | ✅ Sim, especificação oficial | ❌ Legado, criado como solução temporária |
Ideal para aplicações web | ✅ Totalmente | ❌ Foi adaptado via bundler |
Para o contexto de web (que é o foco desse artigo) o ESM já nasce com uma vantagem imensa em relação ao CJS, ele foi planejado e projetado para ser nativo nos navegadores.
O ESM logo ganhou tração e os pacotes e projetos web passaram por refatorações para dizer adeus ao CJS e olá ao ESM, e aqui para variar temos outro problema.
Os fantasmas do passado sempre te perseguem
Não é um mal limitado aos humanos essa questão dos erros do passado continuarem existindo no futuro, isso aconteceu com a web também! e nesse caso vamos falar de como o CJS continua assombrando a web mesmo com uma década de ESM(considerando a data de criação desse post).
- Muitos pacotes que foram escritos com CJS não foram refatorados para ESM, e isso acaba gerando uma verdadeira salada de frutas com o projeto usando pacotes ESM e pacotes CJS (tipos diferentes de sintaxe de execução, CJS usa require enquanto ESM usa import, maneiras diferentes de como lidam com o carregamento de módulo e entre outros pontos.)
- Uma complicação em fazer pacotes CJS funcionar no web, vide o Jest (talvez esse seja um pouco pessoal) que precisa de um arquivo de configuração com um
mapper
explicando para a lib como ela deve tratar pacotes ESM na hora da execução dos testes. - Frameworks conhecidos com versões mais recentes não dão mais nenhum tipo de suporte ao CJS (não é mesmo Angular v17+), pegando como exemplo o Angular v17+ que vem por padrão com o ESBuild como bundler, você pode se deparar com essa surpresa ao buildar uma aplicação com o ESBuild que ainda tem pacotes no CJS:
- Todo esse problema entre CJS e ESM reforça mais um dos problemas que listamos no inicio desse post, a necessidade de ficar "alinhando" as versões das dependências do seu projeto e torcer MUITO para que o
npm start
funcione como estava funcionando antes e não que alguns desses dizeres se apresente no console:
ReferenceError: exports is not defined in ES module scope
SyntaxError: Cannot use import statement outside a module
TypeError: Module "x" does not provide an export named "default"
As vezes me pego pensando de que a web ainda está em um momento de "velho oeste" aonde as coisas ainda estão se estabelecendo mas não existe muito uma lei a ser seguida... e você? acha que as coisas já estão estabelecidas ou que ainda existirão mudanças drásticas assim como mudamos de CJS para ESM?
Bom esse foi um breve resumo de como os módulos surgiram na web e de como eles podem facilitar e complicar muito a sua vida. Espero que tenha gostado, nos vemos por aí 🤠.
Top comments (0)