Un cinema di particelle senza dipendenze
Per vent'anni ho costruito lavoro che nessuno vede. Questa è la parte visibile, smontata con la stessa disciplina.
Il lavoro infrastrutturale ha un'estetica strana: fatto bene, nessuno se ne accorge. Un backup gira in silenzio per vent'anni, poi un giorno non gira più, e quel giorno tutti imparano che esiste. È questa la misura del lavoro invisibile: viene giudicato solo quando cede.
Questo sito è il tentativo di portare in superficie quell'abitudine. Ciò che segue non è decorazione, è la cosa smontata: cosa c'è, perché c'è e quanto costa.
Prima viene il vincolo
Una sola regola ha deciso tutto: nessuna richiesta esterna.
Non è uno slogan, è un'intestazione di risposta. La politica di sicurezza del sito recita:
Content-Security-Policy: default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;
connect-src 'self'; object-src 'none'; base-uri 'self';
form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests
Nel momento in cui scrivi default-src 'self', le opzioni si chiudono. Niente GSAP, niente three.js, niente Lenis, nemmeno un carattere tirato da una CDN. Vuoi una libreria di animazione? La scriverai. Vuoi lo scorrimento smorzato? Lo scriverai. Vuoi un motore di particelle — sì, anche quello.
La regola si è dimostrata da sé una volta, in modo inatteso. Cloudflare inietta il proprio script di analisi al bordo della rete. La politica del sito lo ha bloccato:
Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/…'
violates the following Content Security Policy directive: "script-src 'self'"
Su un server locale Lighthouse dà 100 in Best Practices; dal vivo dà 93. Quei sette punti sono esattamente questo. Allentare la politica sarebbe stato facile. Si è invece spento il beacon — perché una regola significa qualcosa solo quando vale anche nel momento scomodo.
Dove vivono le particelle
Tutto il movimento della pagina è disegnato su un solo <canvas> con WebGL2. Posizioni e velocità delle particelle non stanno sulla CPU; vivono in due texture, e a ogni fotogramma uno shader di frammento le fa avanzare. Il risultato viene scritto in una texture, e al fotogramma successivo quella texture diventa l'ingresso. Il trucco più antico della GPGPU: fare il calcolo da texture a texture, mai verso lo schermo.
In pratica significa che il numero di particelle non riguarda la CPU. Su desktop il campo gira a piena densità, su telefono scende a 12.100 particelle — ma in entrambi i casi il lavoro per fotogramma è la stessa chiamata di shader.
Tenere due texture è un dettaglio piccolo ma portante. Non si può leggere e scrivere la stessa texture in un solo passaggio; è comportamento indefinito. Perciò due buffer si alternano: questo fotogramma legge A e scrive B, il successivo scambia i ruoli. Questo ping-pong è l'igiene di base della GPGPU.
Il colore è un calcolo a parte. La scena viene disegnata in HDR, le zone luminose vengono estratte e sfocate (bloom selettivo), poi una curva ACES riporta tutto nell'intervallo visualizzabile. Quei tre passaggi evitano l'errore più facile del disegno additivo: che tutto bruci verso il bianco. La suite di verifica misura esattamente questo — la luma di picco per fotogramma deve restare sotto soglia.
Le differenze fra dispositivi sono gestite da una scala a quattro gradini:
| Grado | Cosa si spegne |
|---|---|
| 0 | Niente — cinema pieno |
| 1 | Niente zoom-blur, bloom a due passaggi |
| 2 | Il numero di particelle scende al 55% |
| 3 | Ripiego WebGL1 — GPGPU del tutto spenta |
Il grado 3 conta: in un browser senza WebGL2 la pagina non smette di funzionare, perde soltanto lo strato di simulazione. Testo, navigazione, contatti — tutto resta.
Otto lingue, una sola fonte di verità
Il sito esce in otto lingue: il turco alla radice, le altre sette nelle proprie cartelle. Ma otto pagine non si scrivono a mano.
La pagina turca è il master. Ogni nodo traducibile porta un contrassegno:
<span data-i="about.e1t">Kurumsal altyapı</span>
Un generatore legge quei contrassegni ed estrae i18n/tr.json — attualmente 102 chiavi. Le altre sette lingue riempiono le stesse chiavi con il proprio testo, e il generatore stampa le pagine. Anche i campi traducibili del JSON-LD passano dalla stessa pipeline: qualifica, città, aree di competenza, descrizione del servizio.
La regola più dura di questo impianto: una chiave mancante ferma la build. Nessun ripiego, nessuna caduta sull'inglese. Se una lingua resta indietro, non esce nulla. Meglio fermarsi che pubblicare una pagina tradotta a metà in silenzio.
Anche il fatto che la pagina turca non venga generata è voluto. Il master si scrive a mano perché porta commenti di misura, margini di maschera e note di allineamento ottico — spostarli dentro un generatore li renderebbe illeggibili.
Il risultato è misurabile: fra le otto pagine i collegamenti hreflang sono reciproci in 64 direzioni su 64, e x-default è corretto su tutte e otto.
I caratteri sono la parte più pesante
L'intera pagina pesa 172 KB su undici risorse. La ripartizione sorprende:
| Voce | Peso |
|---|---|
| Caratteri (woff2) | 116 KB |
| JavaScript | 56 KB |
| Immagini | 0 KB |
| CSS | 0 KB — in linea |
Sul sito non c'è un solo <img>. Tutto il carico visivo è canvas e SVG. Di contro ci sono quindici file woff2: quattro famiglie per il latino, sostituti separati per il cirillico. I file cirillici sono legati agli stessi nomi di famiglia tramite unicode-range — la pagina russa quindi non scarica mai un file latino; è il browser a scegliere.
Le dichiarazioni @font-face sono incorporate nell'HTML. È una decisione misurata: un foglio di stile separato era l'unica risorsa che bloccava il rendering, e incorporarlo ha portato il punteggio Lighthouse mobile da 99 a 100.
Trasferimento reale dopo compressione:
| Risorsa | Grezzo | Brotli | Rapporto |
|---|---|---|---|
| HTML | 87.890 B | 20.675 B | 76,5% |
app.js | 186.011 B | 55.354 B | 70,2% |
Senza misura nulla si dice «fatto»
Il repository tiene due suite di verifica: 72 test per la fisica della scena, 100 per la traduzione e lo strato di scoperta. Entrambe girano in un browser vero e nessuna guarda uno screenshot — leggono la luma dal canvas, interrogano i canali di stato e controllano le soglie con i numeri.
La ragione è semplice: la verifica visiva mente. Un pannello può scorrere da solo, una scheda finire in secondo piano, requestAnimationFrame non aver mai battuto un colpo. Lo screenshot dice «funziona» mentre non è cominciato nulla.
Come mente la misura
La cosa che ha richiesto più tempo qui non è stato il codice. È stato imparare a non fidarsi della misura stessa.
Un esempio. Una pagina si è aperta in un browser di automazione e il contenuto è rimasto invisibile per settanta secondi. Schermo nero, il sipario d'apertura fermo. La lettura ovvia: la pagina si è bloccata.
Prima di trarre quella conclusione sono state misurate tre cose:
document.visibilityState → "hidden"
fotogrammi rAF in 2 sec → 0
avanzamento apertura → 0
La scheda era in secondo piano. requestAnimationFrame non batte nelle schede in background; la sequenza non era mai partita. Sulla pagina non c'era alcun errore — l'errore era nell'ambiente di misura.
Mancava un passo perché finisse in un rapporto come «riscontro». Il repository ha una regola scritta apposta: non chiamarlo fatto senza misurare — e misura anche ciò che credi di aver visto. Uno screenshot non è prova; la prova è la luma letta dal canvas, o il canale di stato stesso.
Due guasti silenziosi
Le cose più interessanti che un audit porta a galla sono quelle che non stava cercando.
Primo: il campo lastmod nella sitemap leggeva la data dalla sitemap precedente. Il generatore prendeva il proprio output come input. Una volta scritto, il valore non sarebbe mai avanzato — per quanto cambiasse il contenuto, il file avrebbe mostrato per sempre lo stesso giorno.
Un segnale di freschezza congelato diventa una bugia. Un lastmod stantio è peggio di nessuno: dice al crawler che questa pagina non cambia. Ora la data vive in un solo posto, e la sitemap la segue.
Secondo: l'elenco dei campi JSON-LD traducibili era duplicato nel generatore in due punti — uno per l'estrazione, uno per la scrittura. Cosa succede se un campo nuovo viene aggiunto solo a uno? Il campo non viene tradotto, in silenzio. Il testo turco finisce copiato tale e quale nei dati strutturati di sette pagine, e nessuno dei cento test se ne accorge.
Era una trappola che avrebbe rotto proprio ciò che doveva riparare: chi aggiunge il campo description mancante avrebbe scritto turco in sette lingue senza accorgersene. Ora l'elenco è una fonte sola.
La lezione comune: i guasti più pericolosi di un sistema sono quelli in cui nessun test fallisce.
Il trucco rifiutato
La riga più onesta di questo progetto non sta in un rapporto di bug. Sta in un verbale di decisione.
Su mobile Lighthouse non riesce a produrre il valore Largest Contentful Paint per la home. La causa è la sequenza d'apertura: quattro secondi di sipario, poi il congelamento al contatto, poi l'esplosione, poi la transizione d'inchiostro. Prima di allora nessun testo del DOM è visibile, e Lighthouse chiude la traccia molto prima. Risultato: NO_LCP — la metrica non è calcolabile.
Una correzione è stata misurata. Portando l'opacità iniziale dello strato di contenuto a 0.004 invece di 0, l'LCP scende da 9.052 ms a 164 ms, e l'intero titolo viene marcato come candidato LCP. Non c'è costo visivo: 0,004 di opacità contribuisce al massimo 1/255 di unità, cioè nulla.
Ed è esattamente per questo che è stata rifiutata.
L'LCP è definito come il più grande blocco di testo visibile nel viewport. Un titolo a 0,004 di opacità non è visibile. L'eccezione opacity: 0 di Chromium esiste per bloccare proprio questo schema; 0,004 sguscia lungo il suo bordo. Il numero sarebbe migliorato, la verità no: il visitatore avrebbe comunque visto il titolo nove secondi dopo.
La decisione è entrata nella documentazione così: non riportare indietro questo schema.
Perché questa pagina è diversa
La pagina che state leggendo non carica il motore descritto sopra. Il campo di particelle dietro è uno script separato e molto piccolo; il testo è pienamente visibile alla prima pittura e non attende dietro alcun cancello di opacity.
Il motivo è la sezione precedente. Un testo esiste per essere letto e citato. L'apertura cinematografica è la firma della home, ma su una pagina di testo la stessa sequenza ritarderebbe non solo la misura ma la lettura. Per questa sezione c'è un budget scritto — LCP sotto 2,5 secondi, JavaScript di pagina sotto 60 KB, nessun cancello di opacità sul contenuto — e la suite di verifica lo controlla a ogni esecuzione.
Stessa disciplina, esito diverso. Se l'estetica del lavoro invisibile è che non cede mai, l'estetica del lavoro visibile è che si può misurare.