Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Snapshot di riferimento del capitolo
Questo capitolo usa il candidato 2.35.2, branchfeat/ui-ux-2.35.2, commit4d32a8c2a6cd291e4fe389bdf10fdbaf1856a8c2.
Il manifest conserva ancora2.35.1perché il bump appartiene alla preparazione finale della release.
Nei capitoli precedenti abbiamo incontrato quasi tutti i problemi che, prima o poi, diventano problemi di performance:
MutationObserver;Finché li osserviamo separatamente sembrano dettagli locali.
Quando li mettiamo insieme emerge invece un fatto importante:
Una browser extension non gira “dentro JavaScript”. Gira dentro un browser che deve contemporaneamente eseguire JavaScript, gestire DOM e layout, disegnare, ricevere input, fare rete, decodificare immagini, mantenere cache e coordinare più context.
Per questo dire semplicemente:
"questa funzione è lenta"
è quasi sempre una diagnosi troppo povera.
Potrebbe essere:
CPU-bound
I/O-bound
memory-bound
main-thread-bound
DOM-bound
network-bound
oppure una combinazione delle precedenti.
Il nostro obiettivo in questo capitolo non sarà quindi collezionare micro-ottimizzazioni.
Costruiremo invece un metodo per rispondere a quattro domande:
Per PlumePilot è utile separare almeno quattro budget.
Quanto lavoro computazionale deve svolgere il processo:
parsing
ordinamenti
classificazione pagine
hash SHA-256
pixel analysis
costruzione DOM
compressione ZIP
Quanto attendiamo risorse esterne:
fetch API
PDF remoti
asset immagini
storage browser
messaggi fra context
Quanti dati sono vivi contemporaneamente:
PDF bytes
PDF.js document
canvas
pixel buffer
immagini codificate
cache
stringhe HTML
JSZip
output finale
Per quanto tempo il main thread rimane indisponibile per:
input utente
paint
layout
scroll
animazioni
aggiornamenti UI
Questi budget sono collegati, ma non equivalenti.
Una modifica può fare questo:
più parallelismo
↓
meno wall time
↓
più richieste contemporanee
↓
più buffer vivi
↓
picco RAM maggiore
Oppure:
cache più aggressiva
↓
meno rete
↓
meno scansioni
↓
più memoria trattenuta
Il concetto centrale del capitolo è quindi:
Ottimizzare significa scegliere quale risorsa stiamo cercando di risparmiare.
Supponiamo che un export duri 20 secondi.
Non sappiamo ancora quasi nulla.
Potrebbe contenere:
2 s CPU
18 s rete
oppure:
18 s CPU
2 s rete
Dal punto di vista dell’utente il wall time è simile.
Dal punto di vista architetturale sono due sistemi completamente diversi.
E c’è un terzo tempo:
perceived latency.
Un’operazione di 20 secondi che aggiorna progressivamente:
Download dispensa 3/12
Conversione pagina 8/24
Compressione 64%
può sembrare più controllabile di un’operazione da 12 secondi che congela completamente l’interfaccia.
Quindi:
performance != solo durata totale
ma anche:
responsiveness
feedback
cancellabilità
progressività
performance.now() e clock monotonoNel convertitore EPUB troviamo:
const startedAt = performance.now();
Riferimento:
epub-core.mjs:1842
E alla fine:
diagnostics.elapsedMs = performance.now() - startedAt;
Riferimento:
epub-core.mjs:2119–2121
Per una misurazione di durata questo è preferibile a:
Date.now()
perché performance.now() usa un riferimento monotono pensato per misurare intervalli.
Questo ci permette di introdurre una regola semplice:
Date.now()
→ quando ci interessa il tempo civile / timestamp
performance.now()
→ quando ci interessa quanto è durata un'operazione
Nel repository esistono benchmark EPUB.
Ma uno dei commenti più importanti è questo:
// Supplementary renderer benchmark. These timings are NOT browser measurements.
Riferimento:
scripts/benchmark-epub-node.mjs:1
E il benchmark Chromium specifica:
scope:
"Chromium converter over HTTP; not an installed-extension integration or process-memory test"
Riferimento:
scripts/benchmark-epub-regions.mjs:95–101
Questo è software engineering molto più importante del singolo numero prodotto dal benchmark.
Un benchmark deve dichiarare cosa misura e cosa non misura.
Altrimenti potremmo concludere:
"il benchmark Node usa 120 MB"
quindi:
"l'estensione nel browser usa 120 MB"
ma non sarebbe una conclusione valida.
Node, Chromium, PDF.js worker, canvas implementation e extension runtime possono avere ownership e accounting della memoria differenti.
Sono attività correlate ma diverse.
Risponde a domande come:
Quanto dura questa conversione?
Quanti byte produce?
Quante immagini vengono deduplicate?
La variante A è più veloce della B?
Risponde invece:
Dove viene speso il tempo?
Quale funzione alloca?
Quale oggetto trattiene memoria?
Quale task blocca il main thread?
Un benchmark può dirci:
versione nuova: 14.2 s
versione vecchia: 19.6 s
ma non necessariamente perché.
Un profiler ci aiuta a costruire quel perché.
Per un workflow di PlumePilot le metriche utili possono essere:
elapsedMs
numero richieste API
numero retry
numero fallback visuali
pagine text/regional/visual
input image bytes
unique image bytes
EPUB bytes
numero cache hit/miss
numero capitoli aperti nel DOM
numero canvas creati
peak working set
Non tutte devono finire nel prodotto.
Molte possono vivere solo nei benchmark o in modalità diagnostica.
Il punto è un altro:
Una performance regression è molto più facile da individuare quando abbiamo una metrica legata al comportamento che vogliamo proteggere.
O(N) possono avere costi radicalmente diversiNel Capitolo 8 abbiamo visto la ricerca della prima attività incompleta.
Una scansione completa dei capitoli è, concettualmente:
O(N)
Anche questa strategia è O(N):
leggi master index
ordina candidati
ispeziona i moduli necessari
Eppure possono avere prestazioni molto differenti.
Perché?
Perché l’unità di costo non è sempre:
una iterazione JavaScript
Può essere:
aprire accordion
attendere rendering LMS
fare una request HTTP
attendere una detail call
eseguire querySelectorAll
far cambiare route alla SPA
Quindi per sistemi I/O-heavy una formula più realistica è:
Costo ≈
operazioni economiche × costo economico
+ operazioni costose × costo costoso
Se riduciamo le detail call da 50 a 5, possiamo ottenere un enorme guadagno pur restando formalmente in O(N).
La funzione:
findNextPlaybackTargetViaApi(...)
costruisce discoveryOrder.
Quando è disponibile il course index, prima colloca i capitoli sospetti:
const suspectedIncomplete = (index) => {
const route = routeByLessonNumber.get(outline[index].lessonNumber);
return !route || Number(route.percentage) < 100;
};
discoveryOrder = [
...discoveryOrder.filter(suspectedIncomplete),
...discoveryOrder.filter((index) => !suspectedIncomplete(index)),
];
Riferimento:
content.js:5896–5911
La complessità asintotica non diventa magica.
Ma cambia il numero atteso di operazioni costose eseguite prima di trovare il target.
Questo è un esempio di:
cost-based optimization.
Non stiamo semplicemente contando elementi.
Stiamo cercando di ordinare le operazioni per ridurre il costo medio.
Nel percorso non-bookmark troviamo anche:
if (
!options.bookmarkSearch &&
!options.forwardVideoSearch &&
indexedChapter &&
Number(indexedChapter.percentage) >= 100
) {
continue;
}
Riferimento:
content.js:5947–5953
Il master index qui non deve raccontarci ogni dettaglio.
Deve rispondere a una domanda più economica:
vale la pena fare la richiesta dettagliata?
Questa è la funzione classica di un indice.
querySelectorAll() non è gratisIn content.js incontriamo molte query.
Per esempio:
document.querySelectorAll("video").forEach(attach);
oppure ricerche più ampie su:
span
div
heading
button
row
Il costo di una query DOM dipende da:
Una query occasionale può essere irrilevante.
La stessa query ripetuta ad ogni mutazione di un grande subtree può diventare una hot path.
MutationObserver globale di autoplayIl codice attuale usa:
new MutationObserver(scan).observe(document.documentElement, {
childList: true,
subtree: true,
});
Riferimento:
content.js:7076–7079
Ogni volta che viene invocato scan():
document.querySelectorAll("video").forEach(attach);
const activeRow = currentLesson();
...
Riferimento:
content.js:7066–7074
È una soluzione robusta perché non dipende dal framework interno della piattaforma.
Ma ha un potenziale costo:
molte mutazioni DOM
↓
molti callback observer
↓
nuove scansioni
Questo non significa automaticamente che sia un problema misurabile.
Significa che è un candidate hotspot.
La regola è:
Prima profilare, poi restringere l’observer se esiste un beneficio reale.
Il pattern più efficiente concettualmente non è:
MutationObserver = il dato
ma:
MutationObserver = qualcosa potrebbe essere cambiato
Quindi:
mutation
↓
invalidate / schedule
↓
fresh query
Questo modello era già emerso nel Capitolo 3.
Dal punto di vista delle performance permette anche strategie come:
debounce
coalescing
narrow observer root
specific mutation filtering
se un profiler dimostrasse che servono.
Supponiamo che il framework LMS produca 30 mutazioni durante un singolo aggiornamento visuale.
Non sempre vogliamo eseguire 30 volte:
scan();
Possiamo invece trasformare:
mutation
mutation
mutation
mutation
in:
schedule scan once
Questa tecnica si chiama spesso coalescing.
PlumePilot la applica in vari punti attraverso funzioni schedule...() e timer, anche se non ogni observer usa necessariamente lo stesso meccanismo.
Operazioni come:
element.getBoundingClientRect()
sono molto diverse da leggere una semplice proprietà JavaScript.
In PlumePilot vengono usate per capire:
Il problema non è chiamare getBoundingClientRect().
Il problema nasce soprattutto quando alterniamo ripetutamente:
DOM write
layout read
DOM write
layout read
perché il browser potrebbe dover aggiornare layout più volte.
Il principio generale è:
Quando possibile, raggruppare reads e writes invece di interlevarle senza necessità.
Promise.all() ovunqueSe dobbiamo processare 40 capitoli, potremmo pensare:
await Promise.all(chapters.map(loadChapter));
Sembra la soluzione più veloce.
Ma può creare contemporaneamente:
40 richieste
40 response buffer
40 parse
40 cache write
40 possibili retry
Il wall time può diminuire.
Il carico complessivo può aumentare drasticamente.
Inoltre stiamo interagendo con un servizio terzo che non controlliamo.
PlumePilot usa spesso una strategia più conservativa.
Fra le costanti troviamo:
const API_MATERIAL_PACING_MS = 150;
const TURBO_API_PACING_MS = 350;
Riferimento:
content.js:60–61
Durante la raccolta materiali:
if (index + 1 < outline.length) {
await exportSleep(API_MATERIAL_PACING_MS, operationId);
}
Riferimento:
content.js:3172–3175
Questo è un esempio di backpressure applicativa.
La pipeline dice:
posso chiedere il prossimo elemento
ma sceglie deliberatamente di non saturare il producer.
Sembra un paradosso.
Inserire 150 ms:
rende la singola sequenza più lenta
ma può:
ridurre burst di richieste
ridurre retry
ridurre errori transitori
ridurre concorrenza sui buffer
ridurre carico sulla piattaforma
rendere cancellazione e progress più prevedibili
La performance di un sistema non è soltanto:
velocità massima in condizioni ideali
ma anche:
stabilità sotto carico e condizioni imperfette
PlumePilot separa:
const COURSE_INDEX_RETRY_DELAYS_MS = [350, 750];
const API_LESSON_RETRY_DELAYS_MS = [750, 1500];
Riferimento:
content.js:62–63
Un retry serve a recuperare un fallimento.
Il pacing serve a regolare il ritmo normale.
Sono due meccanismi diversi.
Confonderli produce algoritmi strani:
"aspettiamo perché potrebbe fallire"
invece di:
pacing → comportamento normale
retry → recupero eccezionale
Nel builder quiz esiste:
const imageCache = new Map();
Riferimento:
test-builder.js:16
Quando viene richiesta un’immagine:
if (imageCache.has(url)) return imageCache.get(url);
poi viene salvata la Promise della richiesta, non soltanto il risultato finale.
Questo dettaglio è importante.
Se due consumer richiedono simultaneamente la stessa URL:
consumer A ─┐
├→ stessa Promise → una fetch
consumer B ─┘
anziché:
consumer A → fetch
consumer B → fetch
Questa è in-flight deduplication.
Nelle pipeline API il log esplicita casi come:
reused from memory cache
Il beneficio di una cache non è soltanto diminuire latenza.
Può eliminare:
HTTP
parse JSON
normalizzazione
DOM fallback
attese
retry
Per questo una cache ben progettata può migliorare più budget contemporaneamente.
Ma, come abbiamo visto nel Capitolo 6, sposta il costo verso la memoria.
Nel MAIN-world interceptor troviamo:
const pageLessonSnapshots = new Map();
La cache viene limitata:
if (pageLessonSnapshots.size > 50) {
pageLessonSnapshots.delete(pageLessonSnapshots.keys().next().value);
}
Riferimento:
commission-interceptor.js:414–415
Questa è una proprietà fondamentale:
bounded growth.
Senza limite, se la pagina continua a visitare identità nuove:
1 entry
10 entries
100 entries
1000 entries
...
il processo non ha nessuna ragione applicativa per liberarle.
Come visto nel Capitolo 6, una Map conserva l’ordine di inserimento.
Cancellare:
map.keys().next().value
rimuove l’entry inserita prima fra quelle ancora presenti.
Quindi questo schema è più vicino a:
FIFO eviction
che a:
LRU eviction
perché una semplice lettura non sposta automaticamente la chiave in fondo.
Questo può essere perfettamente adeguato.
Una LRU vera costerebbe maggiore complessità.
Una struttura dati può essere veloce per entry e comunque pericolosa se cresce senza limite.
Per esempio:
```textlookup O(1)-ish
non ci dice nulla su:
```text
numero massimo di entry
La domanda di performance corretta è quindi doppia:
quanto costa una operazione?
quanto può crescere la struttura?
Nel Capitolo 9 abbiamo visto le rappresentazioni intermedie dell’EPUB.
Ora le guardiamo dal punto di vista della memoria.
Durante la conversione possono coesistere:
PDF Uint8Array
PDF.js document
pagina PDF.js
textContent
operator list
canvas sorgente
ImageData temporaneo
canvas di regione
immagine compressa
XHTML
JSZip entries
EPUB finale Uint8Array
L’insieme di dati necessari in questo momento è il:
working set.
Il problema reale non è soltanto quanti byte avremo alla fine.
È quanti byte dobbiamo mantenere vivi contemporaneamente.
Definiamo:
peak memory = massimo working set raggiunto durante l'operazione
Due algoritmi possono produrre lo stesso EPUB da 40 MB ma avere:
Algoritmo A → picco 150 MB
Algoritmo B → picco 600 MB
Il file finale non ci permette di dedurlo.
Questo è il motivo per cui una riduzione della dimensione dell’EPUB non dimostra automaticamente una riduzione della RAM.
Il changelog 2.35.1 è infatti prudente:
File-size gains depend on the document and do not establish RAM savings.
Riferimento:
CHANGELOG.md:9
Una immagine PNG da 800 KB può derivare da un canvas che, durante il rendering, rappresenta milioni di pixel non compressi.
Per un buffer RGBA 8-bit il modello mentale è:
bytes ≈ width × height × 4
PlumePilot limita il rendering visuale con:
const VISUAL_MAX_PIXELS = 5000000;
const VISUAL_MAX_DIMENSION = 8192;
Riferimento:
epub-core.mjs:22–23
Cinque milioni di pixel RGBA corrispondono già a circa:
20,000,000 bytes
≈ 19.1 MiB
solo per una rappresentazione pixel equivalente.
E non stiamo ancora contando:
ImageData temporanei;La funzione:
function visualRenderScale(viewport, renderWidth = VISUAL_RENDER_WIDTH) {
return Math.min(
renderWidth / viewport.width,
Math.sqrt(VISUAL_MAX_PIXELS / (viewport.width * viewport.height)),
VISUAL_MAX_DIMENSION / Math.max(viewport.width, viewport.height),
);
}
Riferimento:
epub-core.mjs:1101–1107
è una misura di performance molto più importante di quanto sembri.
Non dice:
crea il canvas grande e poi riducilo
Dice:
calcola prima un upper bound
↓
alloca direttamente entro il budget
È una forma di admission control sulle allocazioni.
getImageData() crea altro lavoro e altri datiNella conversione regionale:
const pixels = context.getImageData(
0,
band.start,
canvas.width,
band.end - band.start,
).data;
Riferimento:
epub-core.mjs:1453–1461
getImageData() restituisce dati pixel per la regione richiesta.
Quindi una strategia che analizza pixel non ha solo costo CPU.
Ha anche costo di memoria e copying/representation.
Per questo il codice lavora per band, non necessariamente sempre sull’intera pagina in un unico ImageData aggiuntivo.
rowInkDurante la classificazione regionale troviamo:
const rowInkCache = new Int32Array(canvas.height).fill(-1);
const cachedRowInk = (y) => {
if (rowInkCache[y] < 0)
rowInkCache[y] = rowInk(context, y, 0, canvas.width);
return rowInkCache[y];
};
Riferimento:
epub-core.mjs:1388–1392
È una piccola memoization molto mirata.
Calcolare l’ink di una riga richiede leggere pixel.
La pipeline può chiedere più volte la stessa riga durante:
inkBands
refinement
cut search
Quindi spendiamo:
O(height) interi
per evitare lavoro pixel ripetuto.
È un ottimo esempio di trade-off:
un po' più memoria
→ molta meno CPU potenziale
Una frase comune è:
"JavaScript ha il garbage collector, quindi non devo liberare memoria"
È sbagliata.
Il GC decide quando liberare oggetti non più raggiungibili.
Il programmatore decide, direttamente o indirettamente, quali riferimenti restano raggiungibili.
Se manteniamo:
cache.set(key, hugeBuffer);
quel buffer resta raggiungibile.
Se un event listener chiude sopra un grande oggetto, può mantenerlo raggiungibile.
Se un array cresce indefinitamente, gli elementi restano raggiungibili.
Quindi il problema non è chiamare manualmente free().
È progettare correttamente:
ownership
lifetime
reachability
1×1Dopo il rendering PlumePilot esegue pattern come:
finally {
canvas.width = 1;
canvas.height = 1;
}
Riferimenti:
epub-core.mjs:1132–1135
epub-core.mjs:1194–1203
epub-core.mjs:1546–1563
Questo non è l’equivalente JavaScript di free().
È però una maniera esplicita per dire:
non abbiamo più bisogno della grande backing surface
ridimensionando il canvas a una superficie minima.
La lezione architetturale è:
Quando una risorsa browser ha un lifetime chiaramente terminato, renderlo evidente nel codice può ridurre la retention accidentale.
page.cleanup()Ogni pagina PDF.js viene gestita così:
const page = await pdf.getPage(pageNumber);
try {
...
} finally {
page.cleanup();
}
Riferimento:
epub-core.mjs:1684–1762
Questa è una delle ragioni per cui il finally non serve soltanto agli errori.
Serve alla resource lifetime correctness.
Qualunque percorso avvenga:
successo
fallback
errore
cancellazione
il cleanup deve avere una via di esecuzione.
pdf.destroy()Alla fine della dispensa:
if (destroyPromise) {
await destroyPromise;
} else if (pdf) {
await pdf.destroy().catch(() => {});
} else if (loadingTask) {
await loadingTask.destroy().catch(() => {});
}
Riferimento:
epub-core.mjs:1775–1783
Il codice gestisce anche il caso di cancellazione durante il caricamento.
Quindi non abbiamo soltanto:
lifetime normale
ma anche:
lifetime interrotto
Un sistema cancellabile deve liberare risorse anche lungo i percorsi che non arrivano alla fine naturale.
Per collegare l’AbortSignal al render PDF.js:
signal?.addEventListener("abort", cancel, { once: true });
ma nel finally:
signal?.removeEventListener("abort", cancel);
Riferimento:
epub-core.mjs:1082–1096
Stesso principio in yieldToBrowser() e download/retry.
Un listener non è necessariamente costoso.
Un listener dimenticato può però:
mantenere closure
mantenere riferimenti
accumulare callback
creare comportamento duplicato
buildCourseEpub() itera:
for (let index = 0; index < materials.length; index++) {
...
const converted = await convertMaterial(...);
...
}
Riferimento:
epub-core.mjs:1891–1962
Questa scelta controlla la concorrenza.
Un’alternativa sarebbe:
await Promise.all(materials.map(convertMaterial));
ma significherebbe poter avere contemporaneamente:
PDF #1 + canvas
PDF #2 + canvas
PDF #3 + canvas
...
La versione sequenziale sacrifica throughput teorico per contenere il working set.
JSZip accumula il pacchetto logico e alla fine:
const bytes = await zip.generateAsync({
type: "uint8array",
compression: "DEFLATE",
compressionOptions: { level: 6 },
}, ...);
Riferimento:
epub-core.mjs:2098–2117
Durante questa fase abbiamo potenzialmente:
asset registrati in JSZip
+
strutture ZIP
+
output Uint8Array in costruzione
Una pipeline completamente streaming potrebbe ridurre alcuni picchi.
Ma introdurrebbe:
Quindi la domanda corretta non è:
"perché non streammare tutto?"
ma:
"il picco attuale giustifica il costo di una architettura streaming?"
In epub-assets.mjs:
const byHash = new Map();
Ogni immagine viene digestata:
const digest = new Uint8Array(
await crypto.subtle.digest("SHA-256", image.bytes)
);
poi identificata con:
const key = `${image.mediaType}:${extension}:${hash}`;
Riferimento:
epub-assets.mjs:1–20
Se il contenuto è già presente:
riuso metadata
non aggiungo una seconda copia al ZIP
La deduplicazione non è gratis.
Per ogni asset:
leggi bytes
calcola SHA-256
lookup Map
Quindi:
più CPU
in cambio di:
meno asset duplicati
meno ZIP bytes
meno storage interno
È di nuovo un trade-off multi-budget.
Dopo la registrazione:
const asset = await assetRegistry.register(image);
...
image.bytes = null;
Riferimento:
epub-core.mjs:1914–1920
Il commento nel codice è molto esplicito:
// Release duplicate buffers; JSZip retains only the canonical bytes.
Qui avviene un ownership transfer concettuale:
converted.images
↓ register
asset registry / JSZip
↓
converted image buffer non serve più
Impostare image.bytes = null rende quella transizione visibile nel codice.
Il problema che vogliamo evitare è:
JSZip → buffer A
images[] → stesso buffer A
included[] → stesso/altro riferimento
aliases → metadata
Anche se alcuni riferimenti puntano allo stesso oggetto e non duplicano fisicamente i byte, strutture ridondanti rendono più difficile capire quando un buffer può diventare unreachable.
La regola è:
Ogni grande buffer dovrebbe avere un owner chiaramente identificabile.
PlumePilot analizza un sample 64×64:
sample.width = 64;
sample.height = 64;
context.drawImage(canvas, 0, 0, sample.width, sample.height);
poi decide:
return photographic
? { extension: "jpg", mediaType: "image/jpeg", quality: 0.9 }
: { extension: "png", mediaType: "image/png", quality: undefined };
Riferimento:
epub-core.mjs:1036–1050
È una decisione di performance basata sulla rappresentazione.
JPEG può comprimere meglio contenuto fotografico.
PNG preserva bene grafica, testo e bordi netti.
Notiamo l’ordine:
canvas grande
↓
downsample 64×64
↓
classificazione economica
↓
codec
↓
encode
Non viene provato prima PNG, poi JPEG, poi scelto il più piccolo.
Quella strategia richiederebbe due encoding completi.
La soluzione attuale usa una cheap heuristic before expensive operation.
Pattern generale:
cheap classifier
↓
choose expensive path
Lo stesso schema appare nella ricerca attività e nel routing API.
Per una pagina semplice PlumePilot tenta:
converted = textPageToHtml(...);
solo se la classificazione non richiede visual rendering.
Riferimento:
epub-core.mjs:1684–1717
Se il testo è sufficientemente affidabile:
niente canvas ad alta risoluzione
niente raster intera pagina
niente immagine codificata
EPUB più reflowable
Questa è contemporaneamente un’ottimizzazione di:
CPU
memoria
file size
accessibilità del testo
adattabilità e-reader
Possiamo leggere il convertitore come:
FAST PATH
PDF text → XHTML
SLOW PATH
PDF → render canvas → analisi → immagine/regione
L’obiettivo di un fast path non è eliminare il slow path.
È usare il slow path solo quando serve.
La pipeline usa una sequenza di crescente costo:
estrai testo
↓
classifica
↓
conversione testuale
↓
controllo incompletezza
↓
regional analysis
↓
full visual fallback
Questo è lo stesso principio algoritmico incontrato nella ricerca attività:
partire dall’evidenza più economica e aumentare il costo solo quando l’incertezza lo richiede.
JavaScript nel main thread ha semantica run-to-completion.
Se una funzione esegue per troppo tempo senza cedere:
input resta in coda
paint resta in coda
scroll può diventare scattoso
progress UI non aggiorna
Anche se l’algoritmo termina correttamente, l’esperienza può essere pessima.
yieldToBrowser()L’EPUB converter implementa:
const browserYieldChannel = new MessageChannel();
const browserYieldQueue = [];
browserYieldChannel.port1.onmessage = () =>
browserYieldQueue.shift()?.();
poi:
function yieldToBrowser(signal) {
...
browserYieldQueue.push(resume);
browserYieldChannel.port2.postMessage(null);
}
Riferimento:
epub-core.mjs:137–167
Il compito non è diventato parallelo.
È stato spezzato in più task cooperativi.
Supponiamo:
100 ms lavoro continuo
contro:
20 ms lavoro
yield
20 ms lavoro
yield
...
Il secondo percorso potrebbe perfino durare leggermente di più.
Ma consente al browser di intercalare altro lavoro.
Quindi:
throughput leggermente peggiore
responsiveness molto migliore
può essere il trade-off corretto.
Durante la conversione troviamo await yieldToBrowser(signal):
Riferimenti:
epub-core.mjs:1198
epub-core.mjs:1487
epub-core.mjs:1551
epub-core.mjs:1764
Questi sono cooperative scheduling points.
MessageChannel?MessageChannel permette di accodare un messaggio che verrà elaborato in un task successivo.
Il punto non è comunicare con un altro frame.
Qui viene usato come primitiva di scheduling:
current task
↓ postMessage
return to browser/event loop
↓
message task
↓
resume Promise
È un uso interessante di una API nata per channel messaging.
requestIdleCallback() sarebbe migliore?Non automaticamente.
requestIdleCallback() è pensato per lavoro a bassa priorità durante periodi idle, ma non è Baseline su tutti i browser e un callback senza timeout può essere ritardato significativamente.
Per un export richiesto esplicitamente dall’utente vogliamo:
cedere il thread
senza dire:
esegui solo quando il browser ritiene di essere idle
Quindi il modello MessageChannel è ragionevole per questo caso.
Quando l’utente preme:
Annulla creazione EPUB
la cancellazione non è soltanto UX.
Serve anche a fermare:
fetch
retry
PDF render
pixel analysis
compressione futura
allocazioni future
Quindi:
Cancellation è una forma di resource management.
renderPage() collega il signal a PDF.js:
const task = page.render(...);
const cancel = () => task.cancel();
signal?.addEventListener("abort", cancel, { once: true });
Riferimento:
epub-core.mjs:1074–1096
Se la cancellazione restasse soltanto nel pulsante UI, il lavoro pesante continuerebbe.
La cancellazione deve attraversare l’intera pipeline.
Una cancellazione perfetta non è soltanto:
alla fine controllo signal.aborted
Dobbiamo anche chiederci:
quanto tempo passa tra il click e il prossimo cancellation point?
Questa è la cancellation latency.
Più il lavoro è suddiviso in unità ragionevoli:
pagina
regione
retry
sleep breve
più rapidamente possiamo interromperlo.
Immaginiamo due EPUB:
A = 40 MB
B = 20 MB
Non possiamo concludere:
B usa metà RAM
Perché B potrebbe essere stato prodotto con:
più canvas intermedi
più hash
più buffer temporanei
compressione più aggressiva
La dimensione finale misura l’artefatto.
La peak memory misura il processo.
JSZip usa:
compression: "DEFLATE",
compressionOptions: { level: 6 }
Riferimento:
epub-core.mjs:2098–2103
Una compressione più aggressiva potrebbe ridurre file size ma aumentare CPU.
Una compressione minore potrebbe aumentare output ma terminare prima.
Ancora una volta:
"più piccolo" != "più performante"
Promise.all() ha sensoNel test builder vengono caricati in parallelo i font:
const [regularBytes, boldBytes] = await Promise.all([...]);
Riferimento:
test-builder.js:197
Questi sono pochi asset noti e indipendenti.
Qui il fan-out è piccolo e controllato.
La creazione dell’HTML offline usa nested Promise.all() per:
test
→ questions
→ question images
→ answers
→ answer images
Riferimento:
test-builder.js:326–347
Questo rende il codice semplice e può accelerare la preparazione.
Ma per un quiz enorme può aumentare il numero di immagini caricate contemporaneamente.
La imageCache evita download duplicati, ma non impone un concurrency limit globale.
Quindi questa è una possibile area futura da misurare prima di introdurre una queue.
Una futura strategia potrebbe usare:
N worker logici
per esempio:
massimo 4 immagini in flight
ottenendo:
più veloce del seriale
meno memoria del Promise.all illimitato
È lo stesso concetto di connection pool o worker pool lato backend.
Il quiz HTML incorpora immagini come Data URL.
Per ogni immagine:
binary bytes
↓
Base64 string
↓
HTML string finale
Questa scelta è ottima per:
un solo file
uso offline
nessuna cartella asset
ma ha un costo di rappresentazione.
Base64 aumenta la quantità di testo rispetto ai byte binari originali e durante la generazione possono coesistere:
bytes originali
Data URL
payload JSON
HTML finale
Blob finale
Potremmo produrre:
index.html
images/...
runtime.js
riducendo alcune duplicazioni di stringhe.
Ma perderemmo il requisito UX:
scarica un file e aprilo ovunque
Questa è una lezione importante:
Una architettura va valutata rispetto ai requisiti del prodotto, non soltanto alla metrica più facile da ottimizzare.
Nel builder materiali:
const url = URL.createObjectURL(
new Blob([bytes], { type: mime })
);
poi:
setTimeout(() => URL.revokeObjectURL(url), 5000);
Riferimento:
materials-builder.js:75–84
Una Blob URL mantiene accessibile la risorsa associata.
Quindi anche il download ha un lifetime da gestire.
Perché il browser deve avere il tempo di consumare il link di download.
Quindi usiamo:
create
click
remove anchor
revoke later
Il timer non è una misura perfetta universale.
È un compromesso pragmatico fra:
non invalidare il download troppo presto
e:
non mantenere la Blob URL indefinitamente
Un export può raggiungere 300 MB e poi tornare a 80 MB.
Questo è high peak memory.
Un altro può partire da 80 MB e, dopo ogni export, stabilizzarsi a:
100
125
155
190
...
Questo suggerisce retention/leak.
Sono problemi differenti.
Cause tipiche:
Map senza eviction
listener mai rimossi
DOM detachato ancora referenziato
closure
worker non terminati
array cumulativi
cache globali
PlumePilot contiene esempi di difese contro alcuni di questi rischi:
Per investigare retention è utile confrontare:
A — prima dell'operazione
B — dopo l'operazione
C — dopo cleanup / ritorno allo stato iniziale
Se B cresce, è normale.
La domanda è:
C torna vicino ad A?
Non necessariamente byte-per-byte, perché GC, JIT e cache del browser possono variare.
Ma il trend è più informativo del singolo numero.
Il garbage collector decide autonomamente quando eseguire collection.
Quindi:
misura 1 → 180 MB
misura 2 → 220 MB
non significa automaticamente che abbiamo perso 40 MB.
Potrebbe esserci memoria reclaimable che il runtime non ha ancora raccolto.
Per questo conviene guardare:
trend
ripetizioni
heap snapshots
retained objects
non solo un contatore live.
performance.memory non è una verità universaleEsiste una proprietà storica performance.memory, ma è non standard e deprecata.
Una API più moderna è:
performance.measureUserAgentSpecificMemory()
ma ha disponibilità limitata e requisiti di sicurezza/cross-origin isolation.
Quindi nel nostro contesto non la tratteremmo come metrica portabile di produzione.
DevTools rimane fondamentale per l’analisi concreta.
Ogni cache dovrebbe poter rispondere a:
chi la possiede?
quando nasce?
quanto può crescere?
quando scade?
come viene invalidata?
quanto è costosa una entry?
Per lessonApiCache abbiamo TTL semantico.
Per pageLessonSnapshots abbiamo un cap numerico.
Per alcune cache del content script il lifecycle della pagina stessa costituisce un limite naturale.
Questi sono tre metodi differenti per controllare crescita e staleness.
Ricordiamo:
const API_LESSON_CACHE_FRESH_MS = 5 * 60 * 1000;
Riferimento:
content.js:64
Un TTL dice:
questa entry è ancora fresca?
Non necessariamente:
rimuovi fisicamente ogni entry dopo 5 minuti
Quindi TTL e memory bound sono concetti distinti.
Array.from, spread e copieNel codice web moderno scriviamo spesso:
[...document.querySelectorAll(...)]
oppure:
[...arrayA, ...arrayB]
Queste operazioni sono comode e leggibili.
Ma creano nuove strutture.
Nella maggior parte del codice PlumePilot sono trascurabili.
In una hot path con decine di migliaia di elementi potrebbero diventare rilevanti.
La regola non è:
mai usare spread
ma:
Non sacrificare leggibilità per micro-allocazioni finché il profiler non dimostra che sono il collo di bottiglia.
È facile trasformare:
array.filter(...).map(...)
in un loop manuale complesso per risparmiare una allocazione.
Se il 95% del tempo è speso in:
fetch
PDF render
canvas encode
abbiamo ottimizzato la parte sbagliata.
Questo è il significato pratico di premature optimization.
La storia della ricerca della prima attività incompleta lo mostra bene.
Saltare tutti i capitoli master 100% sarebbe più veloce.
Ma se il master è stale può introdurre false negative.
Quindi la versione attuale fa una seconda passata.
Risultato:
leggermente più lavoro nel worst case
ma:
correttezza maggiore
Non possiamo valutare performance senza includere il costo degli errori.
Una buona ottimizzazione in sistemi incerti spesso ha questa forma:
fast path quando l'evidenza è forte
fallback corretto quando l'evidenza è debole
Esempi PlumePilot:
master index → detail call → visual fallback
text EPUB → regional → full visual
cache fresh → API → DOM fallback
Questo pattern produce graceful performance degradation.
Durante l’EPUB vengono inviati update con:
dispensa
pagina
percentuale
compressione
Il progress non riduce CPU.
Ma rende osservabile un’operazione lunga.
Una UI che mostra:
64%
permette all’utente di distinguere:
sta lavorando
da:
si è bloccato
Questa informazione modifica l’esperienza del sistema.
Quando parte la compressione ZIP:
cancellable: false
Riferimento:
epub-core.mjs:2089–2116
La UI comunica:
Finalizzazione in corso…
Riferimento:
materials-builder.js:69–72
Questo è un ottimo esempio di truthful UI.
Non dobbiamo mostrare un controllo di cancellazione se il livello sottostante non può più garantire una cancellazione sicura.
Una pagina con formula complessa può essere convertita in:
text
più velocemente e con meno memoria.
Ma se perdiamo simboli matematici, l’output è sbagliato.
Rasterizzarla integralmente preserva fedeltà ma costa:
CPU
RAM
file size
reflowability
La conversione regionale tenta un punto intermedio.
Possiamo pensare che ogni pagina abbia più obiettivi:
fidelity
selectable text
reflow
memory
CPU
file size
Non esiste sempre una soluzione dominante.
Quindi la funzione di classificazione è un policy engine che sceglie un compromesso.
La modalità regionale tenta:
prosa affidabile → XHTML
formula/diagramma → raster
Questo può evitare di trasformare l’intera pagina in immagine.
Ma richiede più analisi:
Quindi ridurre file size o aumentare reflow può costare più CPU durante la conversione.
Possiamo modellare la conversione di una pagina con:
P = pagina
CPU(P)
MEM(P)
OUTPUT(P)
FIDELITY(P)
La strategia sceglie una modalità:
text
regional
visual
con obiettivo:
minimizzare CPU + MEM + OUTPUT
soggetto a FIDELITY >= soglia accettabile
Naturalmente PlumePilot non risolve matematicamente questa funzione.
Ma il modello ci aiuta a capire cosa sta facendo l’euristica.
PDF.js usa un worker configurato con:
pdfjsLib.GlobalWorkerOptions.workerSrc = ...
Riferimento:
epub-core.mjs:8–11
Questo sposta parte dell’elaborazione.
Ma il browser deve comunque gestire nel context della pagina:
canvas
DOM
encoding callback
JSZip orchestration
progress UI
Quindi:
"usa un worker"
non equivale a:
"il main thread non può più bloccarsi"
Possibili candidati concettuali:
hashing
alcune trasformazioni pure
ZIP
analisi dati non DOM
Ma non tutto può essere spostato facilmente.
Canvas/OffscreenCanvas, compatibilità extension, PDF.js integration e trasferimento di grandi buffer introducono altri trade-off.
Se un giorno spostassimo grandi ArrayBuffer fra worker, dovremmo considerare:
structured clone
contro:
transferable ownership
Copiare un buffer da 50 MB può creare un picco diverso dal trasferirne l’ownership.
È un tema naturale per una futura evoluzione del converter, ma non è necessario introdurlo senza misurazioni che lo giustifichino.
In Manifest V3 il service worker Chromium è effimero.
Questo ha una conseguenza utile:
non possiamo trattare le variabili globali background come una cache eterna
Dal punto di vista memory, la sospensione può liberare lo stato in-memory.
Dal punto di vista performance, una riattivazione ha un costo di startup.
Ancora una volta:
lifetime breve
è contemporaneamente un vantaggio e un vincolo.
PlumePilot passa dati fra:
popup
background
content script
MAIN world
builder page
Ogni boundary implica:
serializzazione / structured clone
scheduling
callback / Promise
Ma ottimizzare il messaging prima di misurare potrebbe essere inutile se i payload sono piccoli e il costo dominante è PDF rendering.
La regola resta:
profile the dominant cost
Un messaggio con:
{ type: "START" }
non è equivalente a trasferire megabyte di dati strutturati.
PlumePilot tende a lasciare i grandi artefatti nel context che li costruisce e usa messaging soprattutto per:
comandi
status
operation metadata
Questo riduce la necessità di spostare grandi buffer fra context.
La cache lessonApiCache vive nel content side che esegue la discovery.
I turboControllers vivono nel MAIN interceptor che possiede le fetch abortable.
Gli asset EPUB vivono nel builder EPUB.
Questo è data locality.
Un dato non viene centralizzato soltanto perché potrebbe essere condiviso.
Meno trasferimenti possono significare:
meno serializzazione
meno coupling
lifetime più semplice
Nel Capitolo 11 abbiamo visto che chrome.storage.onChanged controlla quale parte della UI aggiornare.
Per esempio il floating menu non ricostruisce indiscriminatamente tutto ad ogni change.
Decide:
visualStyle → applyVisualStyle
menuSize → applyMenuSize
layout → applyFloatingMenuLayout
autoplay keys → renderSettings
operation → renderOperation
Riferimento:
floating-menu.js:4588–4664
Questa è una forma manuale di dependency tracking.
Per UI piccole potrebbe essere perfettamente accettabile.
Ma rendering selettivo evita:
DOM churn
focus reset
scroll reset
layout extra
animazioni riavviate
Quindi il beneficio non è soltanto CPU.
È anche stabilità dell’interazione.
menu-ux.css nasce soprattutto per coerenza.
Non è corretto dire automaticamente:
meno CSS duplicato = UI più veloce
Il browser deve comunque applicare stili ai nodi rilevanti.
Il vero beneficio principale è:
una sola regola semantica
meno drift
meno patch contraddittorie
La performance di sviluppo conta quanto quella del runtime.
Esiste anche un’altra performance:
tempo necessario per cambiare il software senza romperlo
Una architettura con:
invarianti chiare
test mirati
moduli condivisi
cache con ownership esplicita
può essere leggermente più verbosa ma ridurre drasticamente il tempo di debugging.
Questo non appare in Chrome DevTools.
Ma è un costo reale del sistema.
epub-core.mjs definisce:
const STUDYWING_DEBUG = false;
const debugLog = (...values) => {
if (STUDYWING_DEBUG) console.info(...values);
};
Riferimento:
epub-core.mjs:5–7
Log dettagliati possono essere preziosi durante diagnosi.
In hot path possono anche:
creare stringhe
serializzare oggetti
intasare DevTools
alterare timing
Quindi è corretto distinguere diagnostica da comportamento normale.
DevTools può cambiare il comportamento osservato:
Non significa che non vada usato.
Significa che dobbiamo dichiarare il setup e ripetere le prove.
Una seconda esecuzione può beneficiare di:
HTTP cache
font cache
JIT
asset già scaricati
cache applicativa
Quindi benchmark utili dovrebbero distinguere almeno:
cold-ish run
warm run
quando questo è rilevante.
Un PDF di 3 pagine può non mostrare un problema che appare a 300 pagine.
Un corso con 5 moduli può non mostrare la crescita di una cache che emerge a 200.
Le fixture devono coprire anche la scala del problema.
Misurare:
start → EPUB bytes ready
con performance.now().
Ripetere più volte.
Separare:
baseline text/visual
regional preservation
Con Performance panel cercare:
PDF render
getImageData
canvas encoding
SHA-256
JSZip compression
long tasks
Non partire dal presupposto che il collo di bottiglia sia quello più intuitivo.
Eseguire:
snapshot A
export
snapshot B
cleanup / chiusura builder
snapshot C
Ripetere export se necessario.
Cercare crescita cumulativa.
Se la memoria non torna:
chi mantiene il riferimento?
Candidati:
Map
array
closure
listener
DOM
JSZip
PDF.js
canvas
Il retained size è spesso più informativo del solo shallow size.
Registrare una sessione lunga e verificare:
quante volte scan() viene invocato?
quanto dura?
quali mutazioni lo generano?
Se il costo è irrilevante, non cambiare l’architettura.
Per uno stesso corso:
numero capitoli
numero detail call
numero accordion aperti
tempo fino al target
Il dato più importante potrebbe non essere il tempo JS.
Potrebbe essere il numero di transizioni LMS evitate.
Costruire una fixture con:
molti test
molte domande
immagini ripetute
immagini uniche
Misurare:
peak requests in flight
peak memory
tempo totale
dimensione HTML
Poi confrontare:
Promise.all illimitato
concurrency limit 4
concurrency limit 8
Solo dopo possiamo decidere se la complessità aggiuntiva vale la pena.
Per chi viene dal backend, possiamo mappare:
Map cache ≈ in-process cache
chrome.storage.local ≈ durable local store
pacing ≈ rate limiting/backpressure
Promise pool ≈ connection/worker pool
MutationObserver ≈ invalidation event source
MessageChannel yield ≈ cooperative scheduler handoff
canvas buffer ≈ large in-memory working buffer
builder ≈ batch worker
Ma l’analogia ha un limite fondamentale.
Nel browser il main thread deve anche:
disegnare
ricevere input
aggiornare DOM
animare
Quindi bloccare CPU ha un costo UX immediatamente visibile.
Possiamo formulare alcune proprietà che vorremmo mantenere.
Una sola dispensa PDF pesante viene convertita alla volta.
Ogni pagina PDF viene cleanup dopo l'uso.
I canvas temporanei vengono ridotti dopo l'encoding.
Gli asset identici vengono conservati una sola volta nel registry.
Le richieste batch sono paced.
La cache evita richieste equivalenti quando il dato è ancora utilizzabile.
Le operazioni lunghe espongono progress/status.
La cancellazione ferma davvero il lavoro sottostante quando dichiarata disponibile.
Queste proprietà sono testabili.
I test più stabili spesso verificano proprietà strutturali:
canvas finale ridotto a 1×1 dopo cancellazione
asset duplicato registrato una volta
cache bounded a 50
cancellation non produce file incompleto
una sola conversione attiva
Sono meno rumorosi di un assertion come:
"deve finire in meno di 1.7 secondi"
che dipende dall’hardware.
scripts/benchmark-epub-node.mjs contiene un test che controlla che i canvas osservati dopo cancellazione abbiano dimensione minima:
width <= 1
height <= 1
Riferimento:
scripts/benchmark-epub-node.mjs:151–176
Questa è una resource invariant test.
Non ci dice quanti MB usa il browser.
Ci dice che un’importante via di cleanup è stata eseguita.
MutationObserverIl global observer è una possibile fonte di lavoro ripetuto.
Prima di riscriverlo:
profiling reale su lezioni lunghe
Se emerge come hotspot:
schedule/coalesce scan
oppure restringere root
Promise.all del quizSolo corsi ricchi di immagini possono dirci se serve un concurrency pool.
Una queue introduce stato e failure handling in più.
Non va aggiunta soltanto perché “sembra più robusta”.
Se l’EPUB resta memory-heavy anche dopo cleanup pagina per pagina, la fase di packaging può diventare il prossimo candidato.
A quel punto potremmo valutare:
streaming ZIP
worker
alternative library
ma con fixture rappresentative.
Potremmo introdurre un piccolo diagnostics object comune:
{
elapsedMs,
networkRequests,
cacheHits,
cacheMisses,
pagesText,
pagesRegional,
pagesVisual,
inputBytes,
outputBytes,
uniqueAssetBytes
}
solo nei benchmark/dev mode.
Questo renderebbe più facile confrontare release diverse senza spargere log ad hoc.
Map con oggettiNon esiste evidenza che Map sia il collo di bottiglia.
Ridurre chiarezza semantica per una micro-ottimizzazione ipotetica sarebbe controproducente.
await sleep()Alcuni rappresentano:
pacing
settling della UI host
retry delay
cancellation-friendly wait
Non sono semplicemente “tempo perso”.
Vanno classificati prima di rimuoverli.
Il rischio di picco RAM è evidente dal tipo di working set coinvolto.
Senza benchmark che dimostri un beneficio sicuro, il seriale è una scelta conservativa sensata.
measureUserAgentSpecificMemory() è interessante per diagnostica, ma la compatibilità è limitata e richiede condizioni specifiche.
Non è necessario introdurre una dipendenza runtime per risolvere un problema che possiamo studiare con benchmark e DevTools.
Prima chiedere:
1. È CPU, rete, DOM o memoria?
2. Quanto dura realmente?
3. È lento o semplicemente lungo?
4. La UI rimane responsiva?
5. Il costo cresce con N?
6. Qual è N nel caso reale?
7. Stiamo ripetendo lavoro?
8. Possiamo evitare il lavoro invece di velocizzarlo?
9. Il parallelismo alzerebbe il working set?
10. Il risultato ottimizzato resta corretto?
Queste domande valgono molto più di partire direttamente da:
"mettiamo una cache"
oppure:
"facciamo Promise.all"
| Tecnica | CPU | Rete | Memoria | Responsiveness |
|---|---|---|---|---|
| Course master index | ↓ | ↓ | leggero ↑ | ↑ |
lessonApiCache |
↓ | ↓ | ↑ | ↑ |
| Pacing | ≈ | burst ↓ | picco ↓/≈ | ↑ |
| Promise.all | wall time ↓ | concurrency ↑ | picco ↑ | dipende |
| Conversione EPUB seriale | wall time ↑ | ≈ | picco ↓ | prevedibile |
| Text fast path | ↓ | ≈ | ↓ | ↑ |
| Regional rendering | ↑ | ≈ | dipende | mitigata da yield |
| Canvas cap | ↓/≈ | ≈ | ↓ | ↑ |
| Asset dedup SHA-256 | ↑ | ≈ | ↓ | dipende |
yieldToBrowser() |
overhead ↑ | ≈ | ≈ | ↑↑ |
| Bounded cache | ≈ | possibile ↑ su eviction | ↓ | ≈ |
| Cancellation | lavoro totale ↓ | richieste ↓ | ↓ | ↑ |
La tabella non contiene leggi universali.
È un modo per ricordare che ogni scelta ha più assi.
Non pensare soltanto a:
operazioni al secondo
Pensa a:
CPU
I/O
memoria
main thread
rete
lifetime
Esempi:
cache hit
skip capitolo 100%
text fast path
asset dedup
non rasterizzare se non serve
Specialmente in pipeline binarie:
PDF
canvas
immagini
ZIP
la memoria massima contemporanea conta più della dimensione finale.
yieldToBrowser() non rende necessariamente la conversione più economica.
La rende più cooperativa.
Più elementi contemporanei significano spesso:
meno wall time
più working set
La concorrenza deve essere un numero scelto, non un riflesso automatico.
finally, destroy(), listener removal e release dei buffer non sono “codice accessorio”.
Sono parte della correttezza temporale e di memoria.
Ogni benchmark dovrebbe dire:
ambiente
fixture
metrica
cosa include
cosa esclude
Altrimenti il numero rischia di diventare folklore.
Supponi un canvas:
2400 × 3000
Usando il modello RGBA 8-bit:
Hai 8 PDF da convertire.
Ogni conversione richiede circa:
70 MB peak working set
3 secondi CPU/I/O misto
Confronta concettualmente:
seriale
Promise.all di 8
pool di 2
pool di 4
Quale sceglieresti su un laptop medio e perché?
Dato:
new MutationObserver(scan).observe(document.documentElement, {
childList: true,
subtree: true,
});
progetta una versione con coalescing che esegua scan() al massimo una volta per task/frame, senza perdere la capacità di rilevare un nuovo <video>.
Trasforma mentalmente:
const cache = new Map();
in una cache con massimo 100 entry.
Poi spiega la differenza fra:
FIFO
LRU
TTL
Vuoi dimostrare che una nuova versione EPUB usa meno memoria.
Scrivi un protocollo di benchmark che specifichi:
Il nested Promise.all() carica troppe immagini contemporaneamente.
Disegna una funzione:
mapWithConcurrency(items, 4, mapper)
senza implementarla necessariamente.
Spiega quali stati deve mantenere.
Trova tre fast path reali di PlumePilot e per ciascuno identifica:
cheap evidence
expensive path evitato
fallback di correttezza
Dopo cinque export misuri:
prima: 80 MB
peak: 300 MB
dopo: 92 MB
secondo export peak: 305 MB
dopo: 94 MB
È sufficiente per dichiarare un leak?
Spiega perché no e cosa misureresti successivamente.
Per misurare intervalli:
https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
Per misure nominate:
https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure
Documentazione:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
L’observer notifica mutazioni del DOM; il costo applicativo dipende soprattutto da cosa facciamo nel callback e da quanto ampia è l’area osservata.
https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
Nel converter viene usato come punto di yield cooperativo fra task.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData
getImageData() restituisce un ImageData con i pixel della regione richiesta.
https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory
È sperimentale / non Baseline e non va trattata come API portabile universale.
Per analisi pratica:
https://developer.chrome.com/docs/devtools/
In questo capitolo abbiamo visto che PlumePilot non ha un singolo problema chiamato “performance”.
Ha molti budget interdipendenti.
Abbiamo seguito:
master index
cache
bounded Map
pacing
retry
DOM scan
MutationObserver
PDF.js
canvas
pixel buffer
text fast path
regional rendering
asset dedup
SHA-256
JSZip
yieldToBrowser
AbortController
cleanup
Il filo comune è questo:
La performance migliore nasce quando il software fa meno lavoro inutile, mantiene vivi meno dati contemporaneamente e restituisce periodicamente il controllo al runtime, senza sacrificare la correttezza.
E soprattutto abbiamo imparato a non confondere:
file più piccolo
con:
meno RAM
oppure:
più Promise parallele
con:
sistema più veloce
oppure:
stessa Big-O
con:
stesso costo reale
Il browser è il nostro runtime.
Ottimizzare significa rispettarne i vincoli invece di fingere che sia soltanto una VM JavaScript.
Nel prossimo capitolo cambieremo nuovamente livello.
Finora abbiamo spesso parlato di:
"il browser"
come se fosse uno solo.
PlumePilot deve invece convivere con:
Chrome / Chromium
Edge / Chromium
Firefox
che condividono gran parte delle WebExtensions API ma differiscono in:
Useremo anche la storia reale delle submission AMO e dei warning Firefox per distinguere:
compatibilità del codice
compatibilità dell'API
compatibilità dello store
compatibilità del processo di review
che sono quattro problemi diversi.