SVG e canvas no HTML: qual usar para desenhar na página
SVG inline, SVG em img e o canvas: o que fica no DOM, o que escala sem borrar, o que pesa menos e o que o leitor de tela consegue ler.
SVG é desenho feito de markup: cada barra de um gráfico é um elemento que entra
na mesma árvore da sua página, e que você seleciona, pinta pelo CSS e escuta com
um addEventListener. Canvas é um retângulo de pixels: você pinta com
JavaScript e, terminado o traço, não sobra elemento nenhum para segurar. A
escolha entre os dois cabe numa pergunta — você precisa mexer em cada parte do
desenho depois de desenhado?
Todos os exemplos são da Biblioteca Municipal Vila Nova, que quer publicar no site o número de empréstimos de cada mês e um campo de busca no acervo. Rodei tudo em Chrome 151 dirigido pelo Playwright, num MacBook com Apple M4 Pro; as saídas coladas aqui são as do terminal.
Peças de montar ou tinta numa tela
Pense em dois desenhos iguais. O primeiro é montado com peças: depois de pronto,
você ainda consegue pegar a barra de março, trocar sua cor e ouvir um clique
nela. Esse é o SVG, cuja sigla significa Scalable Vector Graphics: cada
forma continua sendo um elemento. O segundo é pintado com tinta. Você vê os
pixels, mas não consegue pegar “a barra de março” porque ela deixou de existir
como peça. Esse é o canvas.
A comparação serve para identidade, não para qualidade visual: os dois podem ficar nítidos e os dois podem ser redesenhados. A diferença é o que sobra no DOM. Antes de continuar, imagine que a biblioteca quer mostrar uma dica ao passar o mouse em uma única barra. Em qual desenho você já tem uma peça para receber o evento? Essa resposta é a regra de decisão do artigo inteiro.
O mesmo gráfico, desenhado das duas formas
Seis meses, seis barras. Em SVG, o gráfico é markup que você escreve como
escreveria uma lista — só que com rect e text no lugar de li:
<svg id="grafico-svg" viewBox="0 0 320 180" width="320" height="180" role="img"
aria-label="Empréstimos por mês na Biblioteca Vila Nova: 320 em janeiro, 410 em fevereiro, 380 em março, 520 em abril, 610 em maio e 470 em junho.">
<rect class="barra" data-mes="jan" data-total="320" x="28" y="87" width="34" height="63" fill="#2f7d4f"></rect>
<rect class="barra" data-mes="fev" data-total="410" x="74" y="69" width="34" height="81" fill="#2f7d4f"></rect>
<rect class="barra" data-mes="mar" data-total="380" x="120" y="75" width="34" height="75" fill="#2f7d4f"></rect>
<rect class="barra" data-mes="abr" data-total="520" x="166" y="48" width="34" height="102" fill="#2f7d4f"></rect>
<rect class="barra" data-mes="mai" data-total="610" x="212" y="30" width="34" height="120" fill="#2f7d4f"></rect>
<rect class="barra" data-mes="jun" data-total="470" x="258" y="58" width="34" height="92" fill="#2f7d4f"></rect>
<text x="45" y="168" text-anchor="middle" font-size="12">jan</text>
<text x="91" y="168" text-anchor="middle" font-size="12">fev</text>
<text x="137" y="168" text-anchor="middle" font-size="12">mar</text>
<text x="183" y="168" text-anchor="middle" font-size="12">abr</text>
<text x="229" y="168" text-anchor="middle" font-size="12">mai</text>
<text x="275" y="168" text-anchor="middle" font-size="12">jun</text>
</svg>O viewBox é o sistema de coordenadas interno: “esta imagem tem 320 por 180
unidades”. O width e o height dizem o tamanho na tela. Quando os dois
diferem, o navegador escala tudo — e como escalar um rect é recalcular
retângulo, não esticar pixel, o desenho continua nítido em qualquer tamanho.
O mesmo gráfico no canvas é o contrário: o HTML tem uma linha só, e o desenho inteiro vive no JavaScript.
<canvas id="grafico-canvas" width="320" height="180"></canvas>const emprestimos = [
{ mes: 'jan', total: 320 }, { mes: 'fev', total: 410 }, { mes: 'mar', total: 380 },
{ mes: 'abr', total: 520 }, { mes: 'mai', total: 610 }, { mes: 'jun', total: 470 },
];
const ctx = document.querySelector('#grafico-canvas').getContext('2d');
const escala = 120 / 610;
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
emprestimos.forEach((item, i) => {
const altura = Math.round(item.total * escala);
const x = 28 + i * 46;
ctx.fillStyle = '#2f7d4f';
ctx.fillRect(x, 150 - altura, 34, altura);
ctx.fillStyle = '#000';
ctx.fillText(item.mes, x + 17, 168);
});Na tela, os dois gráficos são indistinguíveis. Por dentro, não têm nada a ver um com o outro.
Cada barra do SVG é um nó; no canvas não sobrou nenhum
Este é o script que abre a página no Chrome e pergunta ao DOM o que existe dentro de cada gráfico:
import { chromium } from 'playwright-core';
import { pathToFileURL } from 'node:url';
const navegador = await chromium.launch();
const pagina = await navegador.newPage();
await pagina.goto(pathToFileURL('grafico.html').href);
const contagem = await pagina.evaluate(() => ({
nosNoSvg: document.querySelectorAll('#grafico-svg *').length,
nosNoCanvas: document.querySelectorAll('#grafico-canvas *').length,
textoDoSvg: document.querySelector('#grafico-svg').textContent.trim().split(/\s+/).join(' '),
textoDoCanvas: document.querySelector('#grafico-canvas').textContent.trim(),
barraDeMaio: document.querySelectorAll('#grafico-svg .barra')[4].getAttribute('height'),
}));
console.log(contagem);
await navegador.close();Doze nós no SVG: seis rect e seis text. Zero no canvas. E repare no
textoDoSvg: os rótulos dos meses são texto de verdade, que o Ctrl+F acha,
que o Google indexa e que o tradutor do navegador traduz. No canvas, jan é um
borrão de pixels com forma de letra.
O barraDeMaio: '120' é a quinta barra respondendo qual é a própria altura. E
uma linha de CSS — .barra:hover { fill: #b03030; } — já pinta a barra sob o
mouse, sem JavaScript nenhum. No canvas, o mesmo efeito custa um
mousemove, uma tabela de coordenadas e um redesenho do gráfico inteiro a cada
movimento do cursor.
A consequência prática aparece no clique. O mesmo ponto da tela, nos dois gráficos:
await pagina.evaluate(() => {
globalThis.alvos = [];
const anotar = (e) =>
globalThis.alvos.push({
onde: e.currentTarget.id,
alvo: e.target.tagName,
mes: e.target.dataset?.mes ?? '(nenhum)',
total: e.target.dataset?.total ?? '(nenhum)',
});
document.querySelector('#grafico-svg').addEventListener('click', anotar);
document.querySelector('#grafico-canvas').addEventListener('click', anotar);
});
// clica no centro da barra de maio nos dois gráficos — mesmo ponto, mesmo desenho
for (const seletor of ['#grafico-svg', '#grafico-canvas']) {
const caixa = await pagina.locator(seletor).boundingBox();
await pagina.mouse.click(caixa.x + 229, caixa.y + 90);
}
console.log(await pagina.evaluate(() => globalThis.alvos));No SVG, o event.target é a barra de maio, e ela chega trazendo o próprio dado
junto — os data attributes que eu pendurei em
cada rect. Montar um tooltip com isso é uma linha. No canvas, o alvo é o
elemento inteiro: para saber que o clique caiu em maio, você teria que guardar
as coordenadas de cada barra num array e comparar offsetX com todas elas na
mão. Isso tem nome — hit testing —, e é o trabalho que o navegador já faz de
graça para você no SVG.
Quatro formas de colocar um SVG na página
O mesmo arquivo lupa.svg — o ícone do campo de busca do acervo — pode entrar
de quatro jeitos, e eles não são intercambiáveis:
<!-- o sprite: fica escondido e guarda o desenho uma única vez -->
<svg width="0" height="0" style="position:absolute">
<symbol id="sprite-lupa" viewBox="0 0 24 24" fill="none"
stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"></circle>
<path d="M16.2 16.2 21 21"></path>
</symbol>
</svg>
<label class="campo-busca">
<!-- 1. SVG inline: o markup é da própria página -->
<svg id="icone-inline" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<path d="M16.2 16.2 21 21"></path>
</svg>
<!-- 2. o mesmo arquivo dentro de uma img -->
<img id="icone-img" src="lupa.svg" width="24" height="24" alt="">
<!-- 3. o mesmo arquivo como imagem de fundo no CSS -->
<span id="icone-fundo" class="icone-fundo"></span>
<!-- 4. use apontando para o sprite -->
<svg id="icone-use" width="24" height="24" stroke="currentColor" aria-hidden="true"><use href="#sprite-lupa"></use></svg>
<input type="search" placeholder="Buscar no acervo">
</label>Em img e em background-image, o SVG é tratado como qualquer outra imagem:
tem cache próprio, não engorda o HTML e é ótimo para ilustração grande que se
repete pouco — o mesmo raciocínio que vale para
imagens em HTML. O preço é que o conteúdo dele fica
fechado: o CSS da página não atravessa a fronteira.
Inline, o desenho é markup seu, com todos os poderes e todo o peso disso. E o
use é o meio-termo: o desenho aparece uma vez no documento e cada uso é uma
referência de uma linha.
currentColor: o ícone que muda de cor junto com o texto
currentColor é a palavra-chave que significa “o valor de color que este
elemento herdou”. Num ícone inline, ela liga o traço do desenho à cor do texto
ao lado — e o ícone passa a acompanhar hover, tema escuro e estado desabilitado
sem uma linha de JavaScript.
.campo-busca { color: #2f7d4f; }
.campo-busca:hover { color: #b03030; }
.icone-fundo { display: inline-block; width: 24px; height: 24px; background: url(lupa.svg) no-repeat; }Para provar que funciona (e onde não funciona), li o stroke calculado dos dois
ícones que são markup e contei quantas partes cada um dos quatro tem, antes e
depois de passar o mouse:
const ler = () =>
pagina.evaluate(() => ({
corDoCampo: getComputedStyle(document.querySelector('.campo-busca')).color,
strokeDoInline: getComputedStyle(document.querySelector('#icone-inline circle')).stroke,
strokeDoUse: getComputedStyle(document.querySelector('#icone-use')).stroke,
nosDentroDoInline: document.querySelectorAll('#icone-inline *').length,
nosDentroDaImg: document.querySelectorAll('#icone-img *').length,
nosDentroDoFundo: document.querySelectorAll('#icone-fundo *').length,
}));
console.log('--- estado normal (color: #2f7d4f) ---');
console.log(await ler());
await pagina.hover('.campo-busca');
console.log('--- com o mouse em cima (color: #b03030) ---');
console.log(await ler());O ícone inline foi de verde para vermelho junto com o campo, e o <svg> que
consome o sprite também — o conteúdo clonado pelo use herda o stroke de quem
o chamou. A img e o background não têm como seguir: nosDentroDaImg e
nosDentroDoFundo são 0, ou seja, do ponto de vista da página aquele ícone não
tem partes. É um retângulo opaco, do mesmo jeito que um
iframe é uma página opaca.
Para o ícone de img ou de fundo mudar de cor você precisaria de outro arquivo
para cada cor — ou de truques de filter, que funcionam mal e envelhecem pior.
É por isso que biblioteca de ícones séria entrega SVG inline.
canvas: um retângulo de pixels e nada por dentro
O elemento canvas não é o desenho: é a superfície. Tudo acontece no
contexto, o objeto que getContext('2d') devolve — e que carrega estado.
fillStyle, font, textAlign valem daí em diante, para todas as chamadas
seguintes, até você trocar. Foi por isso que, no gráfico da biblioteca, eu tive
que voltar a #2f7d4f dentro do laço: o fillText preto da volta anterior
tinha deixado a tinta preta carregada.
Esse estado é a maior diferença de mentalidade em relação ao SVG. No SVG você
descreve o que existe; no canvas você dá ordens em sequência, e a ordem
importa. Um retângulo pintado por cima apaga o que estava embaixo, e não existe
“desfazer” — para mudar uma barra, você limpa com clearRect e redesenha o
gráfico inteiro.
Em troca, o canvas não paga o preço do DOM. Cinquenta mil círculos em SVG são cinquenta mil elementos que o navegador precisa criar, posicionar e manter; em canvas são cinquenta mil chamadas que terminam em pixel e somem.
Onde o canvas ganha: o mapa de consultas do acervo
A biblioteca quer plotar um ponto por consulta ao acervo. Medi as duas formas de
desenhar essa nuvem, do começo da montagem até o navegador entregar o quadro
seguinte — doisFrames() é uma promessa que espera dois requestAnimationFrame
seguidos, e pontos é o array de coordenadas, o mesmo para os dois lados. Média
de cinco execuções, descartando a primeira:
const NS = 'http://www.w3.org/2000/svg';
async function medirSvg() {
const svg = document.querySelector('#mapa-svg');
svg.replaceChildren();
const t0 = performance.now();
for (const p of pontos) {
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x);
c.setAttribute('cy', p.y);
c.setAttribute('r', 2);
c.setAttribute('fill', '#2f7d4f');
svg.append(c);
}
await doisFrames();
return performance.now() - t0;
}
async function medirCanvas() {
const ctx = document.querySelector('#mapa-canvas').getContext('2d');
ctx.clearRect(0, 0, 640, 400);
const t0 = performance.now();
ctx.fillStyle = '#2f7d4f';
for (const p of pontos) {
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
}
await doisFrames();
return performance.now() - t0;
}Este é o número que muda a conversa. Com 500 e com 5.000 marcas, os dois empatam em torno de 16 ms — que é a duração de um quadro a 60 fps. Os dois terminam dentro do mesmo quadro; a diferença não existe para quem está olhando.
Só em 50.000 marcas o SVG estoura o orçamento: 87,6 ms contra 36,2 ms — mais de cinco quadros contra pouco mais de dois. “Canvas é mais rápido” é verdade, mas com um limiar embaixo, e o gráfico de seis barras da biblioteca está a quatro ordens de grandeza desse limiar. Trocar SVG por canvas num gráfico pequeno é pagar em acessibilidade e em código de hit testing por um ganho que o cronômetro não mede.
Acessibilidade: o que o leitor de tela recebe de cada um
Um SVG inline com role="img" e aria-label vira uma imagem com legenda. Um
canvas, por padrão, é uma caixa sem nome. Pedi ao próprio Chrome a árvore de
acessibilidade da página do gráfico, que é literalmente o que o leitor de tela
consome:
const cdp = await pagina.context().newCDPSession(pagina);
await cdp.send('Accessibility.enable');
const { nodes } = await cdp.send('Accessibility.getFullAXTree');
for (const no of nodes) {
if (no.ignored) continue;
const papel = no.role?.value ?? '?';
const nome = no.name?.value ?? '';
if (['RootWebArea', 'none', 'generic', 'InlineTextBox'].includes(papel)) continue;
console.log(papel.padEnd(10), JSON.stringify(nome));
}A primeira linha é o gráfico em SVG: papel image, e o aria-label inteiro
virou o nome. A segunda é o canvas: papel Canvas, nome vazio — quem navega por
áudio recebe “gráfico” e nada mais. As seis últimas linhas são os rótulos dos
meses, que continuam no documento como texto porque são <text> do SVG. Do
canvas não veio nenhuma.
A correção não é abandonar o canvas. São duas opções, e a escolha entre elas tem consequência:
<!-- A. o resumo cabe numa frase -->
<canvas width="320" height="180" role="img"
aria-label="Empréstimos por mês: 320 em janeiro, 410 em fevereiro."></canvas>
<!-- B. o dado vira tabela, escrita dentro do próprio canvas -->
<canvas width="320" height="180">
<table>
<caption>Empréstimos por mês</caption>
<tr><th>jan</th><td>320</td></tr>
<tr><th>fev</th><td>410</td></tr>
</table>
</canvas>O que você escreve entre as tags <canvas> e </canvas> não aparece na
tela — só existe para quem não vê o desenho. Rodando o mesmo dump da árvore num
arquivo com os três canvas — um vazio, um da opção A e um da opção B — e
escondendo também os nós de texto solto:
O canvas cru continua sem nome. O da opção A virou image com legenda. O da
opção B continua sendo Canvas "", mas a tabela inteira apareceu na árvore, com
cabeçalho e células — quem navega por áudio percorre o dado linha a linha.
E se você quiser as duas coisas no mesmo canvas?
<!-- as duas juntas: aria-label por fora, tabela por dentro -->
<canvas width="320" height="180" role="img"
aria-label="Empréstimos por mês: 320 em janeiro, 410 em fevereiro.">
<table>
<caption>Empréstimos por mês</caption>
<tr><th>jan</th><td>320</td></tr>
<tr><th>fev</th><td>410</td></tr>
</table>
</canvas>O Chrome 151 entrega as duas: o nome de imagem e a tabela inteira embaixo
dele. Só que isso é implementação, não contrato. A especificação da ARIA
classifica os descendentes de um role="img" como apresentacionais — o
navegador está autorizado a apagar aquela tabela da árvore, e um leitor de tela
que siga a regra ao pé da letra lê só o aria-label. É a mesma frouxidão que
você já viu no primeiro dump, quando os seis StaticText dos meses apareceram
apesar de estarem dentro de um SVG com role="img".
Vale para o SVG também: quando o gráfico é complexo demais para caber num
aria-label, em vez de descrever, publique a tabela ao lado. O resto do assunto
está em acessibilidade em HTML.
Peso real: o mesmo ícone em SVG, em PNG e em fonte de ícones
Rasterizei o ícone da lupa no Chrome em três tamanhos e baixei uma fonte de ícones inteira para comparar:
import { statSync } from 'node:fs';
for (const arquivo of ['lupa.svg', 'lupa-24.png', 'lupa-48.png', 'lupa-72.png', 'material-icons.woff2']) {
console.log(arquivo.padEnd(22), String(statSync(arquivo).size).padStart(6) + ' B');
}Duzentos bytes. Um SVG de ícone é literalmente menor que o PNG de 24 pixels do mesmo desenho — e serve para 24, 48, 72 e para o cartaz impresso, porque é a mesma descrição geométrica em qualquer escala. A fonte de ícones do Google, para comparação, custa 128 kB para entregar o primeiro ícone. Se a biblioteca usa oito ícones no site inteiro, oito SVGs somam menos de 2 kB.
O gráfico de barras conta a mesma história. Este é o arquivo .svg do gráfico
contra o PNG exportado do canvas em três densidades de tela:
| arquivo | bytes | com gzip | serve para |
|---|---|---|---|
grafico.svg |
1387 | 468 | qualquer tamanho |
grafico-1x.png |
3614 | 3510 | só tela comum |
grafico-2x.png |
9766 | 7882 | só telas 2x |
grafico-3x.png |
16853 | 12190 | só telas 3x |
O SVG comprime para 468 bytes porque é texto repetitivo, e o gzip adora texto repetitivo. O PNG cede pouco — de 3% no 1x a 28% no 3x — porque já é um formato comprimido, e o gzip só arranha o que sobrou. E, para cobrir as três densidades de tela, o PNG te obriga a três arquivos; o SVG é um só, e serve inclusive a densidade que ainda não existe.
Os dois erros que derrubam quem está começando
O primeiro é o mais comum de todos, e nem é sobre desenho: é sobre ordem. Se
o script roda antes de o canvas existir na página, querySelector devolve
null:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Mapa de calor das estantes</title>
<script>
const ctx = document.querySelector('#mapa-estantes').getContext('2d');
ctx.fillRect(0, 0, 40, 40);
</script>
</head>
<body>
<canvas id="mapa-estantes" width="320" height="180"></canvas>
</body>
</html>Leia a mensagem inteira: o problema não é getContext, é o null antes dele. O
querySelector não achou nada porque, naquele instante, o body ainda não foi
lido.
E aqui mora a armadilha da correção: defer não conserta script inline. O
atributo só vale para script com src; num bloco escrito dentro do HTML o
navegador ignora o atributo em silêncio. Rodei as três versões da mesma página — a
original, a mesma com <script defer>, e uma terceira com as duas linhas
movidas para um mapa.js carregado por <script defer src="mapa.js"> (com um
console.log no fim, para provar que desenhou):
for (const arquivo of ['erro-canvas.html', 'erro-defer.html', 'erro-defer-src.html']) {
const pagina = await navegador.newPage();
const linhas = [];
pagina.on('pageerror', (e) => linhas.push(e.message));
pagina.on('console', (m) => linhas.push('console: ' + m.text()));
await pagina.goto(pathToFileURL(arquivo).href);
console.log(arquivo.padEnd(20), linhas.length ? linhas.join(' | ') : '(sem erro)');
await pagina.close();
}A do meio quebra igualzinho à primeira. Só a terceira desenha. Se você não quer
criar um arquivo separado, os dois caminhos honestos são mover o <script> para
o fim do body ou embrulhar o desenho num DOMContentLoaded. O assunto tem lição
própria em script defer e async.
O segundo é específico de SVG e cruel, porque não dá erro nenhum: você cria
a forma com document.createElement e ela simplesmente não aparece.
const svg = document.querySelector('#grafico-svg');
const errado = document.createElement('rect');
errado.setAttribute('x', '28');
errado.setAttribute('y', '10');
errado.setAttribute('width', '34');
errado.setAttribute('height', '20');
svg.append(errado);
const NS = 'http://www.w3.org/2000/svg';
const certo = document.createElementNS(NS, 'rect');
certo.setAttribute('x', '74');
certo.setAttribute('y', '10');
certo.setAttribute('width', '34');
certo.setAttribute('height', '20');
svg.append(certo);
const medir = (el) => `${el.getBoundingClientRect().width} x ${el.getBoundingClientRect().height}`;
console.log({
errado: { namespace: errado.namespaceURI, ehSVGElement: errado instanceof SVGElement, caixa: medir(errado) },
certo: { namespace: certo.namespaceURI, ehSVGElement: certo instanceof SVGElement, caixa: medir(certo) },
});Os dois elementos entraram no documento. Só que o primeiro nasceu no namespace
do HTML — é um <rect> HTML, uma tag desconhecida qualquer, do mesmo naipe de
um <xyz>. Ele não é um SVGElement, o navegador não sabe desenhá-lo, e a
caixa dele mede zero por zero. O segundo nasceu no namespace do SVG e mede
exatamente os 34 por 20 que você pediu.
A tabela de decisão
| o que você vai desenhar | escolha | por quê |
|---|---|---|
| ícone, logo, ilustração | SVG inline ou use |
herda a cor do texto e pesa centenas de bytes |
| gráfico com até alguns milhares de formas | SVG | cada forma é clicável e descrita sem código extra |
| ilustração grande que se repete em várias páginas | SVG em img |
ganha cache do navegador e não engorda o HTML |
| mapa de calor, nuvem de pontos, partículas | canvas | acima de dezenas de milhares de formas o DOM cobra caro |
| jogo, editor de imagem, filtro de foto | canvas | você precisa mesmo é do pixel |
| assinatura, desenho a dedo na tela | canvas | traço livre não vira elemento |
| animação de layout (menu, card, botão) | nem um nem outro | CSS resolve, e roda na GPU |
Quando a dúvida persistir, volte à pergunta do começo: cada parte do desenho precisa de nome, de clique ou de descrição? Se sim, SVG. Se o desenho é uma massa de pixels que ninguém vai selecionar, canvas.
O que vem depois
O próximo passo natural é fazer o gráfico da biblioteca sair de um array de
dados em vez de estar escrito na mão — o mesmo for que você já usa para montar
lista, só que gerando rect com createElementNS. Depois disso, vale ver como
o resto da mídia entra na página em
vídeo e áudio em HTML, e conferir onde este
assunto se encaixa no guia completo de HTML.
Perguntas frequentes
Dá para animar um SVG sem JavaScript?
transition e @keyframes do CSS funcionam nos atributos apresentados como propriedade — fill, stroke-dashoffset, opacity, transform. É assim que se faz aquele traço que se desenha sozinho, sem uma linha de script.Como eu salvo o que foi desenhado no canvas como imagem?
canvas.toBlob(blob => ...), que devolve o PNG pronto para virar download ou upload. Existe também toDataURL(), que devolve a imagem como texto base64 — mais fácil de inspecionar, e cerca de 33% maior que o arquivo binário equivalente.Por que o meu canvas fica borrado no celular?
width e height do elemento são pixels reais, e o CSS estica esses pixels. Numa tela com devicePixelRatio 3, defina o canvas com o triplo do tamanho, aplique ctx.scale(3, 3) e deixe o CSS controlar só o tamanho visível. Um SVG não tem esse problema porque não tem pixel.SVG baixado da internet pode conter script?
script dentro dele. Dentro de img o navegador não executa esse script, mas se você colar o arquivo inline na sua página ele passa a ser código seu. Abra o arquivo num editor de texto antes de colar — SVG é texto, dá para ler.Preciso de uma biblioteca de gráficos para desenhar isso?
for gerando rect. A biblioteca compensa quando entram eixos com escala automática, tooltip, zoom e séries que mudam com o tempo — aí você está reescrevendo a biblioteca sem querer.Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 e Chrome 151.0.7922.34 (headless), MacBook Apple M4 Pro, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — SVG: Scalable Vector Graphics — developer.mozilla.org
- MDN — Canvas API — developer.mozilla.org
- HTML Standard — The canvas element — html.spec.whatwg.org


