Migliore pianificazione JS con isInputPending()

Una nuova API JavaScript che può aiutarti a evitare l'equilibrio tra prestazioni di caricamento e reattività dell'input.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Caricare velocemente è difficile. Al momento, i siti che utilizzano JS per eseguire il rendering dei propri contenuti devono trovare un compromesso tra prestazioni di caricamento e reattività dell'input: eseguire contemporaneamente tutto il lavoro necessario per la visualizzazione (migliore prestazioni di caricamento, minore reattività dell'input) o suddividere il lavoro in attività più piccole per rimanere reattivi all'input e alla visualizzazione (prestazioni peggiori del caricamento e migliore reattività dell'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

Supporto dei browser

  • Chrome: 87.
  • Edge: 87.
  • Firefox: non supportato.
  • Safari: non supportato.

Origine

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'ecosistema JS di oggi viene svolta in un unico thread: il thread principale. Questo fornisce un solido modello di esecuzione agli sviluppatori, ma l'esperienza utente (in particolare la reattività) può risentirne drasticamente 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 della pagina, quest'ultima può eseguire un po' di JavaScript, quindi cedere e ritrasmettere 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. Mentre il browser risponde agli eventi più velocemente, il tempo di caricamento complessivo della pagina viene rallentato. Se cedi troppo spesso, la pagina si carica troppo lentamente. Se registriamo meno spesso, ci vuole più tempo prima che il browser risponda agli eventi degli utenti e gli utenti si sentono frustrati. Non è divertente.

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

Noi di Facebook volevamo sapere come sarebbero le cose se trovassimo un nuovo approccio al caricamento che eliminasse questo frustrante compromesso. Abbiamo contattato i nostri amici di Chrome e abbiamo presentato 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.

Un diagramma che mostra che isInputPending() consente al codice JS di verificare se sono presenti input dell'utente in attesa, senza restituire completamente l'esecuzione 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 dei tecnici di Chrome, le patch sono state rilasciate in seguito a una prova dell'origine, che consente a 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. Ognuna di queste è suddivisa 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();
}

Richiamando processWorkQueue() in un secondo momento in una nuova macroattività tramite setTimeout(), permettiamo al browser di rimanere in qualche modo reattivo all'input (può eseguire gestori di eventi prima che il lavoro riprenda) pur mantenendo un'esecuzione relativa ininterrotta. 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.

Questo 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 elementi diversi dall'input (ad es. dipingere) fino al completamento del lavoro, possiamo aumentare facilmente 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. Se specifichi un oggetto a isInputPending() con includeContinuous impostato su true, è tutto a posto:

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 un valore falso quando viene scelto 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 ciclo 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 nel 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 alla sua effettiva implementazione 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.