Migliore pianificazione JS con isInputPending()

Una nuova API JavaScript che può aiutarti a evitare il compromesso tra le prestazioni di carico e la reattività dell'input.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Caricare velocemente è difficile. Al momento, i siti che sfruttano JS per visualizzare i propri contenuti devono scendere a un compromesso tra le prestazioni di carico e la reattività agli input: eseguire tutto il lavoro necessario per la visualizzazione contemporaneamente (migliori prestazioni di caricamento, peggiore reattività all'input) o suddividere il lavoro in attività più piccole per rimanere reattivi all'input e al disegno (prestazioni di carico peggiori, migliore reattività degli input).

Per eliminare la necessità di scendere a questo compromesso, Facebook ha proposto e implementato l'API isInputPending() in Chromium al fine di migliorare la reattività senza generare rendimento. In base al feedback relativo alla prova dell'origine, abbiamo apportato una serie di aggiornamenti all'API e siamo lieti di annunciare che l'API viene ora distribuita per impostazione predefinita in Chromium 87.

Compatibilità del browser

Supporto dei browser

  • 87
  • 87
  • x
  • x

isInputPending() disponibile in browser basati su Chromium a partire dalla versione 87. Nessun altro browser ha segnalato l'intenzione di spedire l'API.

Contesto

La maggior parte del lavoro nell'attuale ecosistema JS viene svolta su un unico thread: il thread principale. Fornisce agli sviluppatori un modello di esecuzione solido, ma l'esperienza utente (in particolare la reattività) può risentirne drasticamente se lo script viene eseguito a lungo. Ad esempio, se la pagina esegue molto lavoro mentre viene attivato un evento di input, la pagina non gestirà l'evento di input di clic fino al termine dell'operazione.

La best practice attuale prevede di risolvere questo problema suddividendo il codice JavaScript in blocchi più piccoli. Durante il caricamento, la pagina può eseguire un bit di JavaScript, quindi restituire il controllo e ritrasmetterlo al browser. Il browser può quindi controllare la coda degli eventi di input e verificare se c'è qualcosa da comunicare alla pagina. Successivamente, il browser può eseguire nuovamente i blocchi JavaScript man mano che vengono aggiunti. Questa operazione è utile, ma può causare altri problemi.

Ogni volta che la pagina restituisce il controllo al browser, il browser impiega del tempo per controllare la coda degli eventi di input, elaborare gli eventi e selezionare il blocco JavaScript successivo. Mentre il browser risponde più rapidamente agli eventi, il tempo di caricamento complessivo della pagina viene rallentato. Se cediamo troppo spesso, la pagina si carica troppo lentamente. Se produciamo meno spesso, occorre più tempo al browser per rispondere agli eventi utente e le persone si sentono frustrate. Per niente divertente.

Un diagramma che mostra che quando esegui attività JS lunghe, il browser ha meno tempo per inviare gli eventi.

Noi di Facebook volevamo vedere come si presenterebbe un nuovo approccio al caricamento che eliminasse questo fastidioso compromesso. Abbiamo contattato i nostri amici di Chrome e abbiamo elaborato una proposta per isInputPending(). L'API isInputPending() è la prima a utilizzare il concetto di interruzioni per gli input utente sul web e consente a JavaScript di verificare l'input senza cedere al browser.

Un diagramma mostra che isInputPending() consente a JS di verificare se è presente un input utente in sospeso, senza restituire completamente l'esecuzione al browser.

Dato l'interesse dimostrato per l'API, abbiamo collaborato con i nostri colleghi di Chrome per implementare e distribuire la funzionalità in Chromium. Con l'aiuto dei tecnici di Chrome, abbiamo ottenuto le patch dopo una prova dell'origine (che è un modo in cui Chrome può testare le modifiche e ricevere feedback dagli sviluppatori prima di rilasciare completamente un'API).

Abbiamo raccolto i feedback dalla prova dell'origine e dagli altri membri del W3C Web Performance Working Group e abbiamo implementato delle modifiche all'API.

Esempio: uno scheduler

Supponiamo che tu debba eseguire molte operazioni di blocco della visualizzazione per caricare la pagina, ad esempio per generare markup dai componenti, escludere i numeri primi o semplicemente tracciare una rotellina di caricamento. Ognuna di queste è suddivisa in un elemento di lavoro distinto. Utilizzando il pattern dello scheduler, spieghiamo come potremmo elaborare il nostro lavoro in un'ipotetica funzione processWorkQueue():

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();
}

Richiamando processWorkQueue() in un secondo momento in una nuova attività macro tramite setTimeout(), diamo al browser la possibilità di rimanere in qualche modo reattivo all'input (può eseguire gestori di eventi prima della ripresa del lavoro) pur continuando a essere relativamente senza interruzioni. Tuttavia, potremmo perdere la programmazione per molto tempo da parte di un'altra attività che vuole il controllo del loop degli eventi o ottenere fino a QUANTUM millisecondi in più di latenza degli eventi.

Va bene, ma possiamo fare meglio? Assolutamente!

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();
}

Tramite l'introduzione di una chiamata al numero navigator.scheduling.isInputPending(), siamo in grado di rispondere più rapidamente agli input e, allo stesso tempo, garantire che il nostro lavoro di blocco dei display venga senza interruzioni negli altri casi. Se non ci interessa gestire nient'altro che l'input (ad es. la pittura) fino al termine del lavoro, possiamo tranquillamente aumentare anche la lunghezza di QUANTUM.

Per impostazione predefinita, gli eventi "continua" non vengono restituiti da isInputPending(). Sono inclusi mousemove, pointermove e altri. Se vuoi arrenderti anche per questi, non preoccuparti. Se fornisci un oggetto a isInputPending() con includeContinuous impostato su true, puoi 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 creando il supporto isInputPending() nelle loro librerie di pianificazione principali utilizzando una logica simile. Speriamo che questo aiuti gli sviluppatori che usano questi framework a trarre vantaggio isInputPending()dietro le quinte senza riformulazioni significative.

Guadagnare non è sempre un male

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 all'elaborazione degli eventi di input, ad esempio per eseguire il rendering ed eseguire altri script nella 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 iframe multiorigine potrebbe segnalare falsi negativi (ad esempio, isInputPending() potrebbe restituire inaspettatamente false quando scegli come target questi frame). Se il tuo sito richiede interazioni con frame secondari stilizzati, assicurati di ottenere spesso risultati sufficienti.

Presta anche attenzione alle 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 multiorigine, perciò le pagine in background potrebbero interferire con la reattività delle pagine in primo piano. Potresti voler ridurre, posticipare o rendere più spesso quando lavori in background utilizzando l'API Page Visibilità.

Ti invitiamo a utilizzare isInputPending() con discrezione. Se non deve essere eseguita alcuna operazione di blocco degli utenti, sii gentile con gli altri nel loop degli eventi generando più frequentemente. 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 del fatto che gli sviluppatori potranno iniziare a utilizzarlo oggi stesso. È la prima volta che Facebook crea una nuova API web e l'ha passata dall'incubazione delle idee alla proposta di standard fino alla spedizione effettiva in un browser. Vorremmo ringraziare tutte le persone che ci hanno aiutato a raggiungere questo punto e ringraziare tutti gli utenti di Chrome che ci hanno aiutato a realizzare questa idea e a realizzarla.

Foto hero di Will H McMahan su Unsplash.