Una nuova API JavaScript che può aiutarti a evitare il compromesso tra prestazioni di caricamento e reattività degli input.
Il caricamento rapido è difficile. I siti che utilizzano JS per eseguire il rendering dei contenuti attualmente devono fare un compromesso tra il rendimento del caricamento e la reattività all'input: eseguire tutto il lavoro necessario per la visualizzazione contemporaneamente (migliore rendimento del caricamento, peggiore reattività all'input) o suddividere il lavoro in attività più piccole per rimanere reattivo all'input e alla pittura (peggiore rendimento del caricamento, migliore reattività all'input).
Per eliminare la necessità di fare questo compromesso, Facebook ha proposto e implementato
l'API isInputPending()
in Chromium per migliorare la reattività senza
rinunciare a nulla. In base al feedback della prova dell'origine, abbiamo apportato una serie di aggiornamenti all'API e siamo lieti di annunciare che ora l'API viene fornita per impostazione predefinita in Chromium 87.
Compatibilità del browser
isInputPending()
nei browser basati su Chromium a partire dalla versione 87.
Nessun altro browser ha segnalato l'intenzione di implementare l'API.
Sfondo
La maggior parte del lavoro nell'attuale ecosistema JS viene eseguita su un unico thread: il thread principale. Questo fornisce agli sviluppatori un modello di esecuzione solido, ma l'esperienza utente (in particolare la reattività) può risentirne notevolmente se lo script viene eseguito per molto tempo. Ad esempio, se la pagina esegue molti calcoli quando viene attivato un evento di immissione, non gestirà l'evento di immissione del clic finché il calcolo non sarà completato.
La best practice attuale per risolvere il problema consiste nel suddividere il codice JavaScript in blocchi più piccoli. Durante il caricamento, la pagina può eseguire un po' di codice JavaScript, quindi cedere il controllo al browser. Il browser può quindi controllare la coda di eventi di input e verificare se c'è qualcosa che deve comunicare alla pagina. Il browser può quindi tornare a eseguire i blocchi JavaScript man mano che vengono aggiunti. Questo può essere utile, ma può causare altri problemi.
Ogni volta che la pagina restituisce il controllo al browser, quest'ultimo impiega un po' di tempo per controllare la coda di eventi di input, elaborare gli eventi e recuperare il blocco JavaScript successivo. Sebbene il browser risponda più rapidamente agli eventi, il tempo di caricamento complessivo della pagina viene rallentato. Se cedi troppo spesso, la pagina si carica troppo lentamente. Se cedi meno spesso, il browser impiega più tempo a rispondere agli eventi utente e le persone si arrabbiano. Non è divertente.
In Facebook volevamo capire cosa succederebbe se trovassimo un nuovo approccio al caricamento che eliminasse questo spiacevole compromesso. Abbiamo contattato i nostri amici di Chrome e abbiamo avanzato la proposta per isInputPending()
. L'API isInputPending()
è la prima ad utilizzare il concetto di interruzione per gli input utente sul web e consente a JavaScript di verificare la presenza di input senza cedere al browser.
Dato l'interesse per l'API, abbiamo collaborato con i nostri colleghi di Chrome per implementare e rilasciare la funzionalità in Chromium. Con l'aiuto degli ingegneri di Chrome, abbiamo implementato le patch in una prova dell'origine (un modo per Chrome di testare le modifiche e ricevere feedback dagli sviluppatori prima di rilasciare completamente un'API).
Ora abbiamo raccolto i feedback della prova di origine e degli altri membri del W3C Web Performance Working Group e abbiamo implementato le modifiche all'API.
Esempio: un programmatore di yieldier
Supponiamo che tu debba eseguire una serie di operazioni che bloccano la visualizzazione per caricare la pagina, ad esempio generare markup dai componenti, fattorizzare i numeri primi o semplicemente disegnare un'animazione di caricamento accattivante. Ognuno di questi è suddiviso in un
elemento di lavoro distinto. Utilizzando il pattern di pianificazione, vediamo come potremmo elaborare il nostro lavoro in una funzione processWorkQueue()
ipotetica:
const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
if (performance.now() >= DEADLINE) {
// Yield the event loop if we're out of time.
setTimeout(processWorkQueue);
return;
}
let job = workQueue.shift();
job.execute();
}
Se chiamiamo processWorkQueue()
in un secondo momento in una nuova macrotask tramite setTimeout()
, diamo al browser la possibilità di rimanere in qualche modo sensibile all'input (può eseguire gestori di eventi prima che il lavoro riprenda) e allo stesso tempo riesce a funzionare in modo relativamente ininterrotto. Tuttavia, potremmo essere rimossi dalla pianificazione per molto tempo da altri lavori
che vogliono il controllo del loop di eventi o avere fino a QUANTUM
millisecondi aggiuntivi
di latenza dell'evento.
Va bene, ma possiamo fare di meglio? Assolutamente sì!
const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
// Yield if we have to handle an input event, or we're out of time.
setTimeout(processWorkQueue);
return;
}
let job = workQueue.shift();
job.execute();
}
Introducendo una chiamata a navigator.scheduling.isInputPending()
, possiamo rispondere più rapidamente agli input, garantendo al contempo che il nostro lavoro di blocco della visualizzazione venga eseguito senza interruzioni. Se non ci interessa gestire altro
oltre all'input (ad es. la pittura) fino al completamento del lavoro, possiamo facilmente aumentare anche la lunghezza di QUANTUM
.
Per impostazione predefinita, gli eventi "continui" non vengono restituiti da isInputPending()
. tra cui mousemove
, pointermove
e altri. Se ti interessa cedere i diritti anche per questi, non c'è problema. Fornendo un oggetto a isInputPending()
con includeContinuous
impostato su true
, possiamo procedere:
const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
// Yield if we have to handle an input event (any of them!), or we're out of time.
setTimeout(processWorkQueue);
return;
}
let job = workQueue.shift();
job.execute();
}
È tutto. Framework come React stanno integrando il supporto di isInputPending()
nelle loro librerie di pianificazione di base utilizzando una logica simile. Ci auguriamo che questo consenta agli sviluppatori che utilizzano questi framework di poter usufruire di isInputPending()
dietro le quinte senza riscritture significative.
La resa non è sempre negativa
Vale la pena notare che produrre meno non è la soluzione giusta per ogni caso d'uso. Esistono molti motivi per restituire il controllo al browser oltre a elaborare gli eventi di input, ad esempio per eseguire il rendering ed eseguire altri script sulla pagina.
Esistono casi in cui il browser non è in grado di attribuire correttamente gli eventi di input in attesa. In particolare, l'impostazione di clip e maschere complessi per gli iframe cross-origin potrebbe generare falsi negativi (ad es. isInputPending()
potrebbe restituire inaspettatamente false quando si sceglie come target questi frame). Assicurati di eseguire il yielding abbastanza spesso se il tuo sito richiede interazioni con frame secondari stilizzati.
Tieni presente anche le altre pagine che condividono un loop di eventi. Su piattaforme come Chrome per Android, è abbastanza comune che più origini condividano un loop di eventi. isInputPending()
non restituirà mai true
se l'input viene inviato a un frame cross-origin e, di conseguenza, le pagine in background potrebbero interferire con la reattività delle pagine in primo piano. Ti consigliamo di ridurre, posticipare o cedere più spesso quando esegui attività in background utilizzando l'API Visibility Page.
Ti invitiamo a utilizzare isInputPending()
con discrezione. Se non c'è bisogno di eseguire operazioni di blocco degli utenti, fai un favore agli altri utenti del loop di eventi cedendo più spesso. Le attività lunghe possono essere dannose.
Feedback
- Lascia un feedback sulla specifica nel repository is-input-pending.
- Contatta @acomminos (uno degli autori delle specifiche) su Twitter.
Conclusione
Siamo entusiasti del lancio di isInputPending()
e che gli sviluppatori possano iniziare a utilizzarlo oggi stesso. Questa API è la prima volta che Facebook ha creato una nuova API web e l'ha fatta passare dall'incubazione dell'idea alla proposta di standard fino al rilascio in un browser. Vogliamo ringraziare tutti coloro che ci hanno aiutato a raggiungere questo punto e fare un saluto speciale a tutto il team di Chrome che ci ha aiutato a sviluppare questa idea e a renderla disponibile.
Foto hero di Will H McMahan su Unsplash.