Salta al contenuto principale

02 - scheduler.yield() e Long Tasks: Sblocca il Main Thread

02 - scheduler.yield() e Long Tasks: Sblocca il Main Thread: scheduler.yield() e Long Tasks: Sblocca il Main Thread Il browser ha un solo main thread.

Cosa ne pensi di questo articolo?

02 - scheduler.yield() e Long Tasks: Sblocca il Main Thread
Core Web VitalsFoto:Bibek ghoshPexels

In Breve

02 - scheduler.yield() e Long Tasks: Sblocca il Main Thread: scheduler.yield() e Long Tasks: Sblocca il Main Thread Il browser ha un solo main thread.

scheduler.yield() e Long Tasks: Sblocca il Main Thread

Il browser ha un solo main thread. Su questo thread girano, in sequenza, tutto il JavaScript della tua applicazione, il layout e il paint della pagina, e la risposta agli input dell'utente. Il modello di esecuzione di JavaScript e run-to-completion: un task JavaScript, una volta iniziato, viene eseguito fino alla fine senza interruzioni. Se quel task richiede 300ms, per quei 300ms il browser non puo fare nient'altro: non risponde ai click, non aggiorna l'interfaccia, non processa altri eventi.

Questo e il problema fondamentale che causa INP elevato. La soluzione tradizionale era usare setTimeout(fn, 0) o Promise.resolve().then() per "dividere" il lavoro, ma questi approcci hanno problemi di prioritizzazione e non comunicano al browser l'intenzione di cedere il controllo. scheduler.yield() e la soluzione nativa progettata esplicitamente per questo scopo.

Cosa Imparerai

  • Come funziona il main thread e il modello run-to-completion di JavaScript
  • Cosa sono i Long Tasks e come identificarli con Chrome DevTools
  • Come scheduler.yield() differisce da setTimeout e Promise.resolve()
  • Pattern pratici per spezzare task pesanti preservando la priorita
  • scheduler.postTask() per task con priorita esplicita
  • Come misurare il miglioramento INP dopo le ottimizzazioni

Il Problema: Run-to-Completion e Long Tasks

Immagina di avere un event handler che, al click di un bottone, deve processare un array di 10.000 elementi e aggiornare il DOM. Senza ottimizzazioni:

// PROBLEMA: questo handler blocca il main thread per centinaia di ms
document.getElementById('process-btn').addEventListener('click', () => {
  const results = [];

  // Elaborazione sincrona di 10.000 elementi
  for (const item of largeDataset) {
    results.push(expensiveTransform(item)); // ogni chiamata costa ~0.1ms
  }

  // Aggiornamento DOM con tutti i risultati
  renderResults(results);
});

Questo handler esegue in modo sincrono per ~1000ms (10.000 item * 0.1ms). Durante quell'intero secondo, il browser non puo rispondere ad altri input, non puo aggiornare l'animazione di un loading spinner, e non puo processare altri eventi. L'INP per questa interazione sara > 1000ms.

Visualizzare il Problema con Performance Observer

// Monitora i Long Tasks in produzione
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn(`Long Task: ${Math.round(entry.duration)}ms`, {
        startTime: entry.startTime,
        attribution: entry.attribution,
      });
    }
  }
});

observer.observe({ type: 'longtask', buffered: true });

scheduler.yield(): La Soluzione Nativa

scheduler.yield() e una nuova API del Scheduler browser (Baseline 2024, 87% supporto browser) che permette a un task JavaScript di cedere il controllo al browser in modo controllato. Il task puo poi riprendersi dove si era fermato, con la garanzia che il browser abbia avuto la possibilita di processare eventi pendenti.

Ecco come riscrivere l'esempio precedente con scheduler.yield():

document.getElementById('process-btn').addEventListener('click', async () => {
  const results = [];
  const CHUNK_SIZE = 100; // processa 100 elementi per volta

  for (let i = 0; i < largeDataset.length; i += CHUNK_SIZE) {
    // Processa un chunk di elementi
    const chunk = largeDataset.slice(i, i + CHUNK_SIZE);
    for (const item of chunk) {
      results.push(expensiveTransform(item));
    }

    // Cedi il controllo al browser tra un chunk e l'altro
    // Il browser puo ora processare input pendenti, animazioni, ecc.
    await scheduler.yield();
  }

  // Aggiorna il DOM con i risultati finali
  renderResults(results);
});

Con questo pattern, invece di un unico Long Task da 1000ms, abbiamo 100 task da 10ms ciascuno, con "yield points" tra uno e l'altro. Il browser puo rispondere agli input tra un chunk e l'altro, portando l'INP da > 1000ms a < 50ms per la risposta iniziale.

scheduler.yield() vs setTimeout(0) vs Promise.resolve()

La differenza fondamentale tra scheduler.yield() e le tecniche precedenti sta nella prioritizzazione:

Metodo Priorita Comportamento Problema
setTimeout(fn, 0) Bassa Aggiunge alla coda dei macrotask Minimo 4ms di delay, priorita imprevedibile
Promise.resolve() Alta (microtask) Esegue prima di altri task Non cede realmente il controllo al browser
scheduler.yield() Eredita dal task chiamante Cede controllo, mantiene priorita originale Nessuno (tranne il supporto browser del 87%)

Il punto chiave di scheduler.yield() e che mantiene la priorita del task chiamante. Se l'utente ha appena cliccato un bottone (alta priorita), i chunk successivi avranno ancora alta priorita rispetto ad altri task di background. Con setTimeout(0), i chunk verrebbero degradati a priorita bassa, potendo essere superati da altri task.

scheduler.postTask(): Controllo Completo della Priorita

Per scenari piu avanzati, scheduler.postTask() permette di pianificare task con priorita esplicita: 'user-blocking' (critica, es. risposta a input utente), 'user-visible' (importante, es. rendering di contenuto), e 'background' (non urgente, es. analytics, prefetch).

// Task critico: risposta diretta a un input utente
await scheduler.postTask(
  () => {
    updateUIImmediately();
  },
  { priority: 'user-blocking' }
);

// Task visibile: aggiornamento interfaccia non urgente
await scheduler.postTask(
  () => {
    updateSecondaryContent();
  },
  { priority: 'user-visible' }
);

// Task background: analytics, prefetch
await scheduler.postTask(
  () => {
    sendAnalyticsData();
    prefetchNextPage();
  },
  { priority: 'background' }
);

Puoi anche cancellare task pendenti tramite TaskController:

const controller = new TaskController({ priority: 'user-visible' });

// Avvia il task
const taskPromise = scheduler.postTask(
  () => { doWork(); },
  { signal: controller.signal }
);

// Cancella il task se l'utente naviga via
window.addEventListener('beforeunload', () => {
  controller.abort();
});

Pattern Pratici per Framework JavaScript

React: Splitting del Rendering Pesante

function HeavyList({ items }: { items: Item[] }) {
  const [visibleItems, setVisibleItems] = useState<Item[]>([]);

  useEffect(() => {
    const renderChunks = async () => {
      const CHUNK_SIZE = 50;
      const allItems: Item[] = [];

      for (let i = 0; i < items.length; i += CHUNK_SIZE) {
        const chunk = items.slice(i, i + CHUNK_SIZE);
        allItems.push(...chunk);

        // Aggiorna lo stato con il chunk corrente
        setVisibleItems([...allItems]);

        // Cedi il controllo prima del prossimo chunk
        if ('scheduler' in window && 'yield' in scheduler) {
          await scheduler.yield();
        } else {
          // Fallback per browser non supportati
          await new Promise(resolve => setTimeout(resolve, 0));
        }
      }
    };

    renderChunks();
  }, [items]);

  return <ul>{visibleItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

Angular: Heavy Computation in OnInit

@Component({
  selector: 'app-data-table',
  template: `<div *ngFor="let row of processedRows">{{ row.value }}</div>`
})
export class DataTableComponent implements OnInit {
  processedRows: ProcessedRow[] = [];

  constructor(private ngZone: NgZone) {}

  async ngOnInit() {
    // Esegui fuori da Angular zone per evitare change detection
    // ad ogni yield
    await this.ngZone.runOutsideAngular(async () => {
      const CHUNK_SIZE = 100;
      const results: ProcessedRow[] = [];

      for (let i = 0; i < this.rawData.length; i += CHUNK_SIZE) {
        const chunk = this.rawData.slice(i, i + CHUNK_SIZE);
        results.push(...chunk.map(r => this.processRow(r)));

        if ('scheduler' in window) {
          await (window as any).scheduler.yield();
        }
      }

      // Rientra in Angular zone solo per l'aggiornamento finale
      this.ngZone.run(() => {
        this.processedRows = results;
      });
    });
  }
}

Come Verificare il Miglioramento

Dopo aver implementato scheduler.yield(), verifica il miglioramento INP:

  1. Chrome DevTools Performance Panel: registra la stessa interazione prima e dopo l'ottimizzazione. Dovresti vedere molti task brevi invece di uno lungo.
  2. web-vitals.js in produzione: monitora il P75 dell'INP per alcuni giorni dopo il deploy. Un miglioramento tipico e dal 40% al 70% del valore INP.
  3. Interaction to Next Paint trace: nel Performance panel, l'input delay dovrebbe ridursi significativamente, poiche i Long Tasks non bloccano piu il main thread al momento del click.

Supporto Browser e Fallback

scheduler.yield() e supportato da Chrome 115+, Edge 115+, Safari 17+ (87% degli utenti). Per Firefox e browser piu vecchi, usa il fallback new Promise(resolve => setTimeout(resolve, 0)). Crea un'utility function che astrae la differenza:

async function yieldToMain(): Promise<void> {
  if ('scheduler' in window && 'yield' in (window as any).scheduler) {
    return (window as any).scheduler.yield();
  }
  return new Promise(resolve => setTimeout(resolve, 0));
}

Conclusioni

scheduler.yield() e lo strumento piu potente disponibile oggi per migliorare INP su interfacce JavaScript complesse. La tecnica di spezzare i task pesanti in chunk con yield points intermedi e concettualmente semplice ma ha un impatto enorme sull'esperienza utente percepita.

La chiave e identificare prima le interazioni problematiche con Chrome DevTools, poi applicare il pattern chunking+yield solo dove necessario. Over-engineering con yield ovunque puo introdurre overhead inutile; applicalo ai task che sai essere pesanti (elaborazione di grandi dataset, rendering di molti elementi DOM, computazioni costose).

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti