Effect cleanup runs asynchronously in 17

The one behavioural change in a release that deliberately has no features: cleanup functions now run after the screen has been updated rather than before.

useEffect(() => {
  const sub = subscribe(id);

  return () => {
    // 16: runs synchronously during unmount
    // 17: runs after the browser has painted
    sub.unsubscribe();
  };
}, [id]);

The reason is that a synchronous cleanup blocks the visual update, which on a large tree is a visible pause when navigating away. The consequence is that anything reading the DOM in a cleanup may find it already gone, and code that assumed the old timing breaks — which is rare and is the only thing to test for. useLayoutEffect keeps the synchronous behaviour for the cases that need it.