Engenharia
Três truques de browser que agora são dívida técnica
Uma funcionalidade torna-se Baseline Widely Available depois de funcionar em todos os principais motores de browser durante 30 meses. O código existente não deixa de funcionar nessa data. Desaparece um dos motivos para manter um truque. Se o seu suporte de browsers segue Baseline, o código escrito à volta da funcionalidade em falta pode agora ser dívida técnica a eliminar.
Desde outubro de 2026, pode reavaliar três truques em bases de código legadas: um helper manual de cancelamento de pedidos, um contentor flex usado apenas para centrar um estado vazio e um contador de carateres. Estas são as datas dos dados de web-features v3.44.0:
| Funcionalidade | Widely Available desde | Primeiras versões: Chrome / Edge, Firefox, Safari |
|---|---|---|
AbortSignal.any() | 19 de setembro de 2026 | 116, 124, 17.4 |
align-content em layout de bloco | 16 de outubro de 2026 | 123, 125, 17.4 |
Intl.Segmenter | 16 de outubro de 2026 | 87, 125, 14.1 |
AbortSignal.timeout() | 18 de outubro de 2026 | 124, 100, 16 |
Para cada caso, mostramos o que escrevíamos antes, o que podemos escrever agora, o que muda e o que fazer se ainda suporta browsers mais antigos. Os exemplos de pedidos e de contagem de carateres usam implementações cobertas por testes no repositório. O exemplo de alinhamento mede o layout no seu browser. A última secção mostra como descobrir a próxima transição sem ler notas de lançamento.
1. Prazos de pedidos: AbortSignal.timeout() e AbortSignal.any()
Um helper manual de cancelamento cria um controlador, inicia um temporizador, propaga o sinal de cancelamento de quem o chamou e faz a limpeza no fim.
O exemplo carrega um utilizador, as suas funções e a atividade recente como um único grupo. Todos os pedidos ainda pendentes têm de ser cancelados ao sair da rota, ao premir Stop ou ao atingir o prazo. Iniciar de novo cancela o carregamento anterior, e um erro num pedido interrompe os restantes. Os dois ficheiros de código implementam esse contrato. O diagrama interativo executa a versão nativa.
Live example
Cancel three requests with one signal
This example runs in your browser and needs JavaScript. The code it runs is below.
// Before: forward the router's signal, manage the deadline and clean up both.
let current;
export function cancelProfile() {
current?.abort();
}
export async function loadProfile(navigationSignal, { timeout = 2000 } = {}) {
current?.abort(); // a new load cancels the previous one
const load = new AbortController();
current = load;
const signal = load.signal;
// The router owns this signal. Its abort event may have happened before this call.
if (navigationSignal.aborted) throw navigationSignal.reason;
const onNavigationAbort = () => load.abort(navigationSignal.reason);
navigationSignal.addEventListener('abort', onNavigationAbort, { once: true });
const timer = setTimeout(
() => load.abort(new DOMException('The deadline expired.', 'TimeoutError')),
timeout,
);
const get = (path) =>
fetch(path, { signal })
.then((response) => {
if (!response.ok) throw new Error(`${path} answered ${response.status}`);
return response.json();
})
.catch((error) => {
load.abort(error); // all or nothing: stop the other requests
throw error;
});
try {
const [user, roles, activity] = await Promise.all([
get('/demo/user'),
get('/demo/roles'),
get('/demo/activity'),
]);
return { user, roles, activity };
} finally {
clearTimeout(timer);
navigationSignal.removeEventListener('abort', onNavigationAbort);
}
}// Now: the platform combines the router's signal, Stop and the deadline.
let current;
export function cancelProfile() {
current?.abort();
}
export async function loadProfile(navigationSignal, { timeout = 2000 } = {}) {
current?.abort(); // a new load cancels the previous one
const load = new AbortController();
current = load;
const signal = AbortSignal.any([
navigationSignal,
load.signal,
AbortSignal.timeout(timeout),
]);
signal.throwIfAborted(); // do not start work if navigation already cancelled it
const get = (path) =>
fetch(path, { signal })
.then((response) => {
if (!response.ok) throw new Error(`${path} answered ${response.status}`);
return response.json();
})
.catch((error) => {
load.abort(error); // all or nothing: stop the other requests
throw error;
});
const [user, roles, activity] = await Promise.all([
get('/demo/user'),
get('/demo/roles'),
get('/demo/activity'),
]);
return { user, roles, activity };
}O que muda
As duas implementações já tratam a navegação cancelada, os prazos e os motivos de cancelamento concorrentes. Nesta refatoração de código, o ganho é simples: menos código da aplicação para manter.
Em before.js, a aplicação verifica se a navegação já foi cancelada, propaga os eventos de cancelamento posteriores e cria um temporizador. Depois remove o listener e limpa o temporizador em finally. Em now.js, AbortSignal.any() combina os sinais recebidos e throwIfAborted() verifica o seu estado antes de o trabalho começar. AbortSignal.timeout() fornece o prazo sem um temporizador gerido pela aplicação.
A versão manual cria um TimeoutError para o prazo, enquanto a versão nativa o recebe da API. Stop produz AbortError nas duas. A proposta original discute por que motivo as aplicações precisam de distinguir um timeout de um cancelamento intencional. As duas versões também leem os corpos das respostas antes de concluir o carregamento e cancelam explicitamente os pedidos restantes quando ocorre um erro. Promise.all() não cancela os seus membros.
AbortSignal.timeout() conta tempo ativo e pode ficar em pausa enquanto a página está na cache de navegação (back-forward cache), por isso o prazo pode não corresponder ao tempo decorrido no relógio. Cancelar um pedido também não interrompe o trabalho no servidor. Se a operação altera dados, “deixámos de esperar” não significa “falhou”, e repetir automaticamente pode não ser seguro.
Para encontrar esta dívida, procure padrões em vez de nomes de funções: setTimeout e .abort( na mesma função, Promise.race com um temporizador (a pior variante, porque o pedido continua a correr depois de Promise.race terminar), um helper escrito à mão para combinar sinais ou um wrapper de cliente HTTP que trata a opção timeout separadamente de signal. Antes de substituir qualquer um deles, registe o que mais o helper faz, como telemetria, novas tentativas, etiquetas de erro ou um adaptador de biblioteca, porque essas tarefas precisam de ficar a cargo de outro código.
Um sinal de timeout começa a contar quando é criado. As duas versões do exemplo criam um prazo novo para cada carregamento. Se acrescentar novas tentativas, crie outro prazo dentro de cada tentativa em vez de reutilizar o sinal anterior.
Solução de recurso para browsers mais antigos
A versão nativa precisa das duas funcionalidades, por isso a versão mínima real é a mais recente das duas: Chrome e Edge 124, Firefox 124, Safari 17.4. Se suporta versões anteriores, detete os dois métodos e forneça uma solução de recurso para cada um que esteja em falta. Por exemplo, este helper fornece uma solução de recurso para AbortSignal.any():
export function anySignal(signals) {
if (typeof AbortSignal.any === "function") {
return AbortSignal.any(signals);
}
const list = [...signals];
if (list.some((signal) => !(signal instanceof AbortSignal))) {
throw new TypeError("Expected AbortSignal");
}
const controller = new AbortController();
for (const signal of list) {
if (signal.aborted) {
controller.abort(signal.reason);
return controller.signal;
}
}
for (const signal of list) {
signal.addEventListener("abort", () => controller.abort(signal.reason), {
once: true,
signal: controller.signal,
});
}
return controller.signal;
}Este helper verifica se algum dos sinais já foi cancelado e preserva o motivo do cancelamento. Cobre apenas AbortSignal.any(). Se também precisar de uma solução de recurso para AbortSignal.timeout(), lembre-se de que um temporizador comum não reproduz o comportamento de tempo ativo do prazo nativo.
Os listeners só são removidos quando o sinal combinado é cancelado. Se um pedido terminar normalmente, o listener no sinal de quem o chamou mantém-se até um dos sinais ser cancelado, por exemplo quando o prazo expira mais tarde. O documento explicativo de AbortSignal.any() descreve a dificuldade de libertar corretamente estas referências no código da aplicação. Para um sinal de longa duração partilhado por muitos pedidos, como um sinal de rota ou de sessão, use antes a versão manual com limpeza explícita. Quando a sua política de suporte deixar de incluir estas versões dos browsers, apague o helper e chame diretamente os métodos nativos.
2. Conteúdo centrado: align-content em layout de bloco
Um estado vazio tem um título, uma frase e um botão. Para centrar o grupo na vertical num painel de altura fixa, a resposta habitual era Flexbox.
Com align-content: center, o elemento continua a ser um bloco normal. align-content funciona em layouts flex e grid há anos. O que se tornou Widely Available foi o seu efeito nos contentores de bloco. O exemplo compara esse bloco com o contentor flex e mostra as duas folhas de estilo.
Live example
Keep centred content reachable when it overflows
This example runs in your browser and needs JavaScript. The code it runs is below.
.empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-block-size: 14rem;
text-align: center;
}.empty-state {
align-content: center;
min-block-size: 14rem;
text-align: center;
}O que muda
Remover display: flex mantém os elementos filhos no fluxo normal. Verificámos o comportamento do layout em Chromium 152. As medidas abaixo são do exemplo numa janela com 1440 px de largura, com Short text selecionado e Panel height definido para 224 px, salvo indicação em contrário. Os indicadores atualizam-se quando altera estas opções ou a largura da janela.
- O título e o parágrafo têm, cada um,
0.75remde margem vertical. Estas margens colapsam num intervalo de 12 px na versão de bloco. A versão flex tem um intervalo de 24 px porque as margens entre itens flex não colapsam. Se acrescentougapou uma classe modificadora para compensar, reveja essa regra ao remover o contentor flex. - Com
align-items: center, o título flex mede 203 px de largura. Na versão de bloco ocupa toda a largura disponível, 312 px nesta configuração. O parágrafo também ocupa a largura do painel, etext-aligncentra o botão como qualquer outro conteúdo inline. Traduções mais longas podem usar toda a largura de texto do painel. - Mude para Long text e reduza Panel height para 80 px. Com a mesma largura de janela, o título flex começa 65 px acima do limite superior, onde a deslocação não o consegue alcançar. A versão de bloco começa nos 0 px e permite deslocar o conteúdo normalmente. A parte cortada depende do conteúdo e do tamanho do painel, por isso observe o indicador top ao ajustar estes valores. A especificação faz o alinhamento de bloco voltar ao início quando o conteúdo transborda. Flex e grid precisam de
safe centerpara fazer o mesmo.
Qualquer valor diferente de normal também faz o contentor de bloco estabelecer um contexto de formatação independente. As margens do primeiro e do último elemento filho deixam de colapsar através dos limites do painel, e os elementos flutuantes dentro dele ficam contidos. Num estado vazio, é normalmente o que se pretende.
As declarações existentes também podem começar a funcionar. Até agora, align-content num bloco normal não fazia nada, por isso pode existir nas folhas de estilo por acidente: copiado de uma regra flex, esquecido depois de uma refatorização ou definido num reset. Essas declarações passam a ter efeito, e o anúncio da equipa do Chrome recomenda procurá-las. Procure align-content em seletores que não são contentores flex nem grid e remova as declarações que ninguém pretendia aplicar.
Um <img>, <video> ou <canvas> inline assenta na linha de base do texto, com um pequeno espaço por baixo para as partes descendentes das letras. O alinhamento de bloco centra a linha inteira, incluindo esse espaço, pelo que a imagem fica ligeiramente acima do centro. Na verificação em Chromium 152, uma imagem de 80 px num painel de 200 px com uma fonte de 32 px ficou a 54 px do topo e a 66 px do fundo. Com vertical-align: top na imagem, ficou a 60 px do topo e a 60 px do fundo. A opção Image do exemplo interativo permite comparar esse efeito com outros tamanhos de painel. Temani Afif descreve o mesmo efeito.
Nada disto substitui o que Flexbox faz para além da centragem. Se o cartão empurra uma ação para o fundo, distribui espaço entre itens ou muda de direção em ecrãs estreitos, mantenha o contentor flex e verifique de que é responsável antes de o remover.
Solução de recurso para browsers mais antigos
Não é possível detetar esta funcionalidade com uma query. Os browsers anteriores a Chrome 123, Firefox 125 e Safari 17.4 já compreendem align-content por causa de Flexbox, por isso @supports (align-content: center) também é verdadeiro nessas versões. A declaração apenas não tem efeito num bloco.
Por isso, pode fazer uma de duas coisas:
-
Não fazer nada. Num browser mais antigo, o conteúdo fica alinhado ao topo, continua a ser legível e o botão continua a funcionar. Para a maioria dos estados vazios e painéis provisórios, é uma solução de recurso aceitável.
-
Se o posicionamento central for mesmo importante em browsers mais antigos, use Grid, mantenha o alinhamento seguro e os elementos filhos à largura total:
css.empty-state { display: grid; align-content: start; /* used where `safe` is not supported */ align-content: safe center; min-block-size: 14rem; text-align: center; } .empty-state > button { justify-self: center; }Sem um
align-contentexplícito, as linhas da grid esticam para preencher o painel e os três elementos filhos distribuem-se do topo ao fundo.justify-items: centerencolheria o título e o parágrafo comoalign-items: centerfazia na versão flex, por isso só o botão é centrado individualmente. A palavra-chavesafeevita o problema de transbordo descrito acima, mas é mais recente do que a própria grid (MDN: posição em caso de transbordo). Onde não é suportada, o browser ignora essa declaração e mantémstart, o que volta a alinhar o conteúdo ao topo. Os elementos filhos da grid são itens de grid, por isso as suas margens não colapsam e o espaçamento é gerido comgap.
3. Contagem de carateres: Intl.Segmenter
Um campo diz “Até 80 carateres” e um contador mostra quantos faltam. Um contador baseado em text.length mede unidades de código UTF-16, tal como o atributo nativo maxlength. Os exemplos manuais também contam pontos de código com [...text] e combinam sequências de emoji com uma expressão regular.
Intl.Segmenter com granularity: "grapheme" divide o texto nas unidades que uma pessoa vê, seleciona e apaga. O exemplo compara as quatro abordagens, incluindo o truncamento. As implementações aparecem por baixo.
Live example
Count characters without cutting emoji apart
This example runs in your browser and needs JavaScript. The code it runs is below.
export const countLength = (text) => text.length;
export const countCodePoints = (text) => [...text].length;
export function truncateOld(text, max) {
return text.length > max ? `${text.slice(0, max)}…` : text;
}
// The usual home-made fix: glue back what [...text] splits apart. A flag is two regional
// indicators; an emoji may carry a skin tone and a variation selector and be joined to the
// next one with ZWJ; a letter may be followed by combining marks. Every sequence Unicode adds
// needs another branch here.
const CHARACTER =
/\p{RI}{2}|\p{Extended_Pictographic}\p{EMod}?\uFE0F?(?:\u200D\p{Extended_Pictographic}\p{EMod}?\uFE0F?)*|\P{M}\p{M}*/gsu;
export const countWithRegex = (text) => text.match(CHARACTER)?.length ?? 0;
export function truncateWithRegex(text, max) {
const characters = text.match(CHARACTER) ?? [];
return characters.length > max ? `${characters.slice(0, max).join('')}…` : text;
}const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
export function countCharacters(text) {
let count = 0;
for (const _ of graphemes.segment(text)) count++;
return count;
}
export function truncate(text, max) {
let end = 0;
let count = 0;
for (const { index, segment } of graphemes.segment(text)) {
if (count === max) return `${text.slice(0, end)}…`;
end = index + segment.length;
count++;
}
return text;
}O que muda
Até as contagens mais simples divergem:
| Texto | length | Pontos de código | Grafemas |
|---|---|---|---|
Lisboa | 6 | 6 | 6 |
Café (é como um único caráter) | 4 | 4 | 4 |
Café (e + acento combinante) | 5 | 5 | 4 |
| 👍🏽 | 4 | 2 | 1 |
| 👨👩👧 | 8 | 5 | 1 |
| 🇵🇹 | 4 | 2 | 1 |
As duas formas de escrever “Café” parecem idênticas, mas um contador de unidades de código conta-as de forma diferente para o limite, e um emoji de família custa oito. A expressão regular acerta nas linhas desta tabela, mas o exemplo interativo também inclui a bandeira da Escócia e hangul decomposto. A expressão regular divide a bandeira em sete partes em vez de um grafema, e o exemplo de hangul em seis em vez de dois.
O truncamento torna o problema mais fácil de ver. truncateOld("Equipa 🇵🇹 Porto", 8) corta a bandeira a meio e deixa um surrogate isolado, que aparece como um símbolo inválido e nem sequer é texto bem formado. truncate() devolve Equipa 🇵🇹….
Também deixa de manter código. Não há uma dependência para dividir grafemas nem uma expressão regular de emoji feita à mão para atualizar sempre que o Unicode acrescenta uma sequência, porque as regras vêm com o browser.
Antes de disponibilizar o novo contador, verifique estes pontos:
- Quando o contador passa a mostrar grafemas, deixa de coincidir com o limite de unidades de código de
maxlength. O campo pode deixar de aceitar texto enquanto o contador ainda diz que há espaço. Removamaxlengthe valide no seu código, ou mantenha deliberadamente unidades de código em todo o lado. - Se o backend conta bytes ou unidades de código, pode rejeitar um valor que o contador aceita. O Node.js também tem
Intl.Segmenter, por isso a mesma função pode correr dos dois lados. Os limites de armazenamento medidos em bytes continuam a ser uma restrição separada. - A segmentação segue os dados Unicode de cada runtime, e as regras dos grafemas mudam entre versões do Unicode, como explica Nikita Prokopov. Uma sequência de emoji muito recente pode contar como mais de um grafema num browser ou servidor mais antigo, por isso deixe alguma folga no limite em vez de depender de uma concordância exata no limiar.
- As duas formas de escrever “Café” continuam a ter valores de string diferentes, embora agora contem o mesmo.
===, a pesquisa e as verificações de unicidade podem tratá-las de forma diferente. Se isso for relevante, chametext.normalize("NFC")nos pontos em que compara e decida separadamente se deve guardar a forma normalizada. - Os métodos de introdução de texto compõem o texto através de estados intermédios. Mostre que o limite foi ultrapassado e deixe a pessoa corrigi-lo, em vez de cortar o valor a cada tecla.
Solução de recurso para browsers mais antigos
Intl.Segmenter só chegou ao Firefox na versão 125, por isso esta solução de recurso é especialmente relevante se suporta versões ESR mais antigas do Firefox ou browsers incorporados. A alternativa depende da margem de erro que pode aceitar:
function makeCounter() {
if (typeof Intl.Segmenter === "function") return countCharacters;
return (text) => [...text].length; // code points: close, but see the table above
}Contar pontos de código funciona para texto simples e conta a mais nas sequências de emoji e marcas combinantes. Num campo de descrição, isso costuma ser aceitável: a pessoa pode ficar com um pouco menos de espaço do que o prometido, e o servidor, com um motor atual, continua a aplicar a regra correta. Se a contagem tiver de ser exata em todo o lado, carregue um polyfill de segmentação de grafemas apenas quando Intl.Segmenter não existir, para que os browsers atuais não paguem esse custo.
4. Gerir dívida técnica com um alvo Baseline no Browserslist
Provavelmente não tinha estas datas no calendário. O Browserslist, a configuração de browsers-alvo que Babel, Autoprefixer e muitas ferramentas de lint já leem, compreende queries Baseline (guia do web.dev):
# .browserslistrc
baseline widely availableEsta query avança sozinha à medida que as funcionalidades passam a marca dos 30 meses. Executámo-la para três datas com browserslist 4.29.4 e ficámos com a versão mais baixa de cada browser principal:
| Data da query | Chrome / Edge | Firefox | Safari |
|---|---|---|---|
| 1 de outubro de 2026 | 123 | 124 | 17.4 |
| 17 de outubro de 2026 | 123 | 125 | 17.4 |
| 19 de outubro de 2026 | 124 | 125 | 17.4 |
O Firefox 125 entrou a 16 de outubro com Intl.Segmenter e align-content em layout de bloco, e o Chrome 124 a 18 de outubro com AbortSignal.timeout(). O mapeamento está no pacote baseline-browser-mapping, de que o Browserslist depende, por isso o alvo só avança quando atualiza as dependências.
Se preferir alterar o alvo de forma deliberada, fixe uma data:
baseline widely available on 2026-10-19Avançar a data passa então a ser um pull request, e a revisão de código é um bom momento para perguntar que truques o novo alvo torna desnecessários. Os três acima são os que a passagem de 1 de outubro para 19 de outubro permitiu remover.
Verifique o alvo real da sua ferramenta de build
Muitas ferramentas de build leem Browserslist, incluindo webpack, Next.js, Rsbuild e Parcel. Outras têm uma definição própria para o alvo. Verifique a versão e a configuração em vigor antes de assumir que uma política Baseline corresponde ao alvo real do build. Estes são os valores predefinidos documentados em outubro de 2026:
| Ferramenta de build | Baseline por predefinição | Onde configurar |
|---|---|---|
| Vite 8 | Widely Available a 1 de janeiro de 2026 | build.target |
| Vite 7 | Widely Available a 1 de maio de 2025 | build.target |
| Rsbuild 2 | Widely Available a 1 de maio de 2025 | Browserslist; queries Baseline suportadas a partir de 2.1.11 |
| Angular 20–22 | Widely Available; a data depende da versão principal | Política de suporte do Angular e configuração Browserslist do CLI |
| webpack 5 | Sem um alvo Baseline predefinido fixo | Browserslist para o runtime; Babel ou SWC para o código da aplicação |
| Parcel 2 | Sem um alvo Baseline predefinido | Browserslist |
| Next.js | Um conjunto fixo de browsers modernos | Browserslist |
| esbuild | Não; o valor predefinido é esnext | target |
O Vite chama 'baseline-widely-available' ao valor predefinido de build.target, mas a data é fixa para cada versão principal. Não avança com o calendário como a query do Browserslist. O alvo predefinido de janeiro de 2026 do Vite 8 ainda inclui browsers sem as funcionalidades que passaram a Widely Available em outubro. Confirme as versões de browser abrangidas antes de remover os truques acima.
No Vite ou no esbuild, use as versões de browser resultantes para definir build.target ou target. Um ficheiro .browserslistrc, por si só, não configura essas opções. No webpack, mantenha alinhados o alvo do runtime e os alvos do Babel ou do SWC. O Angular CLI também lê Browserslist, mas a política de suporte do framework continua a definir o limite. A transformação de sintaxe também não fornece Web APIs em falta, como AbortSignal.any() ou Intl.Segmenter. Os polyfills continuam a ser uma decisão separada.
Registe o alvo para as pessoas e para as ferramentas de programação com IA
Uma política de suporte que só existe na cabeça de alguém não pode ser seguida por um novo colega, nem por uma ferramenta de programação com IA. Sem um alvo definido, a ferramenta não tem como saber se pode usar AbortSignal.any() ou se deve escrever um helper manual. Mantenha o alvo no repositório, onde ambos o possam ler, e indique-o no ficheiro de instruções para agentes (AGENTS.md, CLAUDE.md ou semelhante). No Vite ou no esbuild, indique a configuração de build. Numa ferramenta que lê Browserslist, a instrução pode ser:
## Browser support
Targets are in `.browserslistrc`: Baseline Widely Available, pinned to a date.
Use platform features inside that target directly, without polyfills or fallbacks.
When the date changes, list workarounds the new target makes unnecessary.As ferramentas de lint transformam a política numa verificação para ambos. O eslint-plugin-compat lê a configuração do Browserslist e assinala APIs JavaScript que os seus alvos não suportam, e o plugin CSS do ESLint tem uma regra use-baseline para CSS.
Antes, agora e solução de recurso
| Antes | Agora | Se suporta browsers mais antigos | |
|---|---|---|---|
| Prazo de pedidos | AbortController + setTimeout + limpeza manual de listeners | AbortSignal.timeout() + AbortSignal.any() | Deteção das duas APIs; solução de recurso para any() acima |
| Conteúdo centrado | Contentor flex só para centrar | align-content: center no bloco | Aceitar o alinhamento ao topo, ou usar grid com safe center |
| Contagem de carateres | length ou pontos de código | Grafemas com Intl.Segmenter | Pontos de código, ou um polyfill carregado a pedido |
Se quiser uma segunda opinião sobre que truques do seu código vale a pena retirar e quais ainda fazem trabalho útil, isso faz parte do que uma Auditoria de Risco Frontend analisa.