Smart Bundling: Como servir o código legado apenas para navegadores legados

Publicados: 2022-03-10
Resumo rápido ↬ Embora o agrupamento eficaz de recursos na Web tenha recebido muita atenção nos últimos tempos, a forma como enviamos recursos de front-end para nossos usuários permaneceu praticamente a mesma. O peso médio do JavaScript e dos recursos de estilo que um site acompanha está aumentando - mesmo que a criação de ferramentas para otimizar o site nunca tenha sido melhor. Com a participação de mercado de navegadores evergreen crescendo rapidamente e os navegadores lançando suporte para novos recursos em sincronia, é hora de repensarmos a entrega de ativos para a web moderna?

Um site hoje recebe uma grande parte de seu tráfego de navegadores evergreen – a maioria dos quais tem bom suporte para ES6+, novos padrões JavaScript, novas APIs de plataforma web e atributos CSS. No entanto, os navegadores herdados ainda precisam ser suportados em um futuro próximo — seu compartilhamento de uso é grande o suficiente para não ser ignorado, dependendo da sua base de usuários.

Uma rápida olhada na tabela de uso do caniuse.com revela que os navegadores perenes ocupam a maior parte do mercado de navegadores — mais de 75%. Apesar disso, a norma é prefixar CSS, transpilar todo nosso JavaScript para ES5 e incluir polyfills para dar suporte a todos os usuários que nos interessam.

Embora isso seja compreensível a partir de um contexto histórico - a web sempre foi sobre aprimoramento progressivo - a questão permanece: estamos desacelerando a web para a maioria de nossos usuários para oferecer suporte a um conjunto cada vez menor de navegadores legados?

Transpilação para ES5, polyfills de plataforma web, polyfills ES6+, prefixação CSS
As diferentes camadas de compatibilidade de um aplicativo Web. (Ver versão grande)

O custo do suporte a navegadores legados

Vamos tentar entender como diferentes etapas em um pipeline de compilação típico podem adicionar peso aos nossos recursos de front-end:

Transpilando para ES5

Para estimar quanto peso a transpilação pode adicionar a um pacote JavaScript, peguei algumas bibliotecas JavaScript populares originalmente escritas em ES6+ e comparei seus tamanhos de pacote antes e depois da transpilação:

Biblioteca Tamanho
(ES6 minificado)
Tamanho
(ES5 minificado)
Diferença
TodoMVC 8,4 KB 11 KB 24,5%
Arrastável 53,5 KB 77,9 KB 31,3%
Luxon 75,4 KB 100,3 KB 24,8%
Video.js 237,2 KB 335,8 KB 29,4%
PixiJS 370,8 KB 452 KB 18%

Em média, os pacotes não transpilados são cerca de 25% menores do que aqueles que foram transpilados até o ES5. Isso não é surpreendente, uma vez que o ES6+ fornece uma maneira mais compacta e expressiva de representar a lógica equivalente e que a transpilação de alguns desses recursos para o ES5 pode exigir muito código.

Polyfills ES6+

Embora o Babel faça um bom trabalho ao aplicar transformações sintáticas ao nosso código ES6+, recursos integrados introduzidos no ES6+ — como Promise , Map e Set , e novos métodos de array e string — ainda precisam ser preenchidos com polyfilled. babel-polyfill como está pode adicionar cerca de 90 KB ao seu pacote minificado.

Mais depois do salto! Continue lendo abaixo ↓

Polyfills da plataforma web

O desenvolvimento moderno de aplicativos da Web foi simplificado devido à disponibilidade de uma infinidade de novas APIs de navegador. Os mais usados ​​são fetch , para solicitação de recursos, IntersectionObserver , para observar com eficiência a visibilidade dos elementos, e a especificação de URL , que facilita a leitura e manipulação de URLs na web.

Adicionar um polyfill compatível com especificações para cada um desses recursos pode ter um impacto notável no tamanho do pacote.

Prefixação CSS

Por último, vamos ver o impacto da prefixação CSS. Embora os prefixos não adicionem tanto peso morto aos pacotes quanto outras transformações de compilação - especialmente porque compactam bem quando Gzip'd - ainda há algumas economias a serem alcançadas aqui.

Biblioteca Tamanho
(minificado, prefixado para as últimas 5 versões do navegador)
Tamanho
(minificado, prefixado para a última versão do navegador)
Diferença
Bootstrap 159 KB 132 KB 17%
Bulma 184 KB 164 KB 10,9%
Fundação 139 KB 118 KB 15,1%
IU semântica 622 KB 569 KB 8,5%

Um guia prático para enviar código eficiente

Provavelmente é evidente onde quero chegar com isso. Se aproveitarmos os pipelines de compilação existentes para enviar essas camadas de compatibilidade apenas para navegadores que as exigem, podemos oferecer uma experiência mais leve para o restante de nossos usuários - aqueles que formam uma maioria crescente - mantendo a compatibilidade com navegadores mais antigos.

O pacote moderno é menor que o pacote legado porque dispensa algumas camadas de compatibilidade.
Bifurcando nossos pacotes. (Ver versão grande)

Essa ideia não é inteiramente nova. Serviços como Polyfill.io são tentativas de dinamicamente polyfill ambientes de navegador em tempo de execução. Mas abordagens como essa sofrem de algumas deficiências:

  • A seleção de polyfills é limitada aos listados pelo serviço — a menos que você mesmo hospede e mantenha o serviço.
  • Como o polyfilling acontece em tempo de execução e é uma operação de bloqueio, o tempo de carregamento da página pode ser significativamente maior para usuários em navegadores antigos.
  • Servir um arquivo polyfill personalizado para cada usuário introduz entropia no sistema, o que dificulta a solução de problemas quando as coisas dão errado.

Além disso, isso não resolve o problema de peso adicionado pela transpilação do código da aplicação, que às vezes pode ser maior que os próprios polyfills.

Vamos ver como podemos resolver todas as fontes de inchaço que identificamos até agora.

Ferramentas que vamos precisar

  • Webpack
    Esta será nossa ferramenta de construção, embora o processo permaneça semelhante ao de outras ferramentas de construção, como Parcel e Rollup.
  • Lista de navegadores
    Com isso, gerenciaremos e definiremos os navegadores aos quais gostaríamos de oferecer suporte.
  • E usaremos alguns plugins de suporte a Browserslist .

1. Definindo navegadores modernos e legados

Primeiro, queremos deixar claro o que queremos dizer com navegadores “modernos” e “herdados”. Para facilitar a manutenção e o teste, é útil dividir os navegadores em dois grupos distintos: adicionar navegadores que exigem pouco ou nenhum preenchimento ou transpilação à nossa lista moderna e colocar o restante em nossa lista legada.

Firefox >= 53; Borda >= 15; Chrome >= 58; iOS >= 10.1
Navegadores que suportam ES6+, novos atributos CSS e APIs de navegador como Promises e Fetch. (Ver versão grande)

Uma configuração de lista de navegadores na raiz do seu projeto pode armazenar essas informações. As subseções “Ambiente” podem ser usadas para documentar os dois grupos de navegadores, assim:

 [modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%

A lista fornecida aqui é apenas um exemplo e pode ser personalizada e atualizada com base nos requisitos do seu site e no tempo disponível. Essa configuração atuará como a fonte de verdade para os dois conjuntos de pacotes front-end que criaremos a seguir: um para os navegadores modernos e outro para todos os outros usuários.

2. Transpilação e Polienchimento ES6+

Para transpilar nosso JavaScript de maneira compatível com o ambiente, usaremos babel-preset-env .

Vamos inicializar um arquivo .babelrc na raiz do nosso projeto com isto:

 { "presets": [ ["env", { "useBuiltIns": "entry"}] ] }

Habilitar o sinalizador useBuiltIns permite que o Babel preencha seletivamente os recursos internos que foram introduzidos como parte do ES6+. Como ele filtra os polyfills para incluir apenas os exigidos pelo ambiente, mitigamos o custo de envio com o babel-polyfill em sua totalidade.

Para que este sinalizador funcione, também precisaremos importar babel-polyfill em nosso ponto de entrada.

 // In import "babel-polyfill";

Isso substituirá a importação grande babel-polyfill por importações granulares, filtradas pelo ambiente do navegador que estamos direcionando.

 // Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …

3. Recursos da Plataforma Web Polyfilling

Para enviar polyfills para recursos da plataforma web para nossos usuários, precisaremos criar dois pontos de entrada para ambos os ambientes:

 require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills

E isto:

 // polyfills for modern browsers (if any) require('intersection-observer');

Esta é a única etapa em nosso fluxo que requer algum grau de manutenção manual. Podemos tornar esse processo menos propenso a erros adicionando eslint-plugin-compat ao projeto. Este plugin nos avisa quando usamos um recurso do navegador que ainda não foi polipreenchido.

4. Prefixação CSS

Finalmente, vamos ver como podemos reduzir os prefixos CSS para navegadores que não exigem isso. Como autoprefixer foi uma das primeiras ferramentas do ecossistema a oferecer suporte à leitura de um arquivo de configuração de lista de browserslist , não temos muito o que fazer aqui.

Criar um arquivo de configuração PostCSS simples na raiz do projeto deve ser suficiente:

 module.exports = { plugins: [ require('autoprefixer') ], }

Juntando tudo

Agora que definimos todas as configurações de plug-in necessárias, podemos montar uma configuração de webpack que as leia e gere duas compilações separadas nas pastas dist/modern e dist/legacy .

 const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };

Para finalizar, criaremos alguns comandos de compilação em nosso arquivo package.json :

 "scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }

É isso. A execução do yarn build agora deve nos fornecer duas compilações, que são equivalentes em funcionalidade.

Servindo o pacote certo para os usuários

Criar compilações separadas nos ajuda a alcançar apenas a primeira metade de nosso objetivo. Ainda precisamos identificar e fornecer o pacote certo aos usuários.

Lembre-se da configuração da lista de navegadores que definimos anteriormente? Não seria bom se pudéssemos usar a mesma configuração para determinar em qual categoria o usuário se enquadra?

Digite browserslist-useragent. Como o nome sugere, o browserslist-useragent pode ler nossa configuração de browserslist e então associar um user agent ao ambiente relevante. O exemplo a seguir demonstra isso com um servidor Koa:

 const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });

Aqui, definir o sinalizador allowHigherVersions garante que, se versões mais recentes de um navegador forem lançadas — aquelas que ainda não fazem parte do banco de dados do Can I Use — elas ainda serão relatadas como verdadeiras para navegadores modernos.

Uma das funções do browserslist-useragent é garantir que as peculiaridades da plataforma sejam levadas em consideração ao combinar os agentes do usuário. Por exemplo, todos os navegadores no iOS (incluindo o Chrome) usam o WebKit como o mecanismo subjacente e serão correspondidos à respectiva consulta de lista de navegadores específica do Safari.

Pode não ser prudente confiar apenas na correção da análise do agente do usuário na produção. Ao recorrer ao pacote herdado para navegadores que não estão definidos na lista moderna ou que possuem strings de agente de usuário desconhecidas ou não analisáveis, garantimos que nosso site ainda funcione.

Conclusão: vale a pena?

Conseguimos cobrir um fluxo de ponta a ponta para enviar pacotes sem inchaço para nossos clientes. Mas é razoável imaginar se a sobrecarga de manutenção que isso adiciona a um projeto vale seus benefícios. Vamos avaliar os prós e contras dessa abordagem:

1. Manutenção e testes

É necessário manter apenas uma única configuração de lista de navegadores que alimenta todas as ferramentas neste pipeline. A atualização das definições de navegadores modernos e legados pode ser feita a qualquer momento no futuro, sem a necessidade de refatorar configurações ou códigos de suporte. Eu diria que isso torna a sobrecarga de manutenção quase insignificante.

Há, no entanto, um pequeno risco teórico associado a depender do Babel para produzir dois pacotes de código diferentes, cada um dos quais precisa funcionar bem em seu respectivo ambiente.

Embora os erros devidos a diferenças nos pacotes possam ser raros, monitorar essas variantes quanto a erros deve ajudar a identificar e mitigar efetivamente quaisquer problemas.

2. Tempo de construção versus tempo de execução

Ao contrário de outras técnicas predominantes hoje, todas essas otimizações ocorrem em tempo de construção e são invisíveis para o cliente.

3. Velocidade progressivamente melhorada

A experiência dos usuários em navegadores modernos se torna significativamente mais rápida, enquanto os usuários de navegadores legados continuam a receber o mesmo pacote de antes, sem quaisquer consequências negativas.

4. Usando recursos de navegadores modernos com facilidade

Muitas vezes evitamos usar novos recursos do navegador devido ao tamanho dos polyfills necessários para usá-los. Às vezes, até escolhemos polyfills menores não compatíveis com especificações para economizar no tamanho. Essa nova abordagem nos permite usar polyfills compatíveis com especificações sem nos preocupar muito em afetar todos os usuários.

Pacote diferencial veiculado em produção

Dadas as vantagens significativas, adotamos esse pipeline de construção ao criar uma nova experiência de checkout móvel para clientes da Urban Ladder, uma das maiores varejistas de móveis e decoração da Índia.

Em nosso pacote já otimizado, conseguimos economizar aproximadamente 20% nos recursos Gzip'd CSS e JavaScript enviados para usuários móveis modernos. Como mais de 80% de nossos visitantes diários estavam nesses navegadores perenes, o esforço feito valeu o impacto.

Recursos adicionais

  • “Carregar Polyfills somente quando necessário”, Philip Walton
  • @babel/preset-env
    Uma predefinição inteligente do Babel
  • Lista de navegadores “Ferramentas”
    Ecossistema de plugins construídos para Browserslist
  • Eu posso usar
    Tabela de marketshare atual do navegador