The rule is very simple IMO, I already described it: if other execution can run while you wait for the value, it's blocking. (Edit: to be specific, other observable execution, so in your thread with write access to state you can see)
> Worse, async vs not async is about latency requirements of the caller,
I disagree. To me, async vs not async is about interruptibility. As you already mentioned, the code that computes a trillion digits of Pi is async if its interruptible, and not async if it isn't. If you're in a UI thread doing 60fps animation, async vs not async isn't really all that relevant (barring the slight inherent overhead in calling an async function). What you need is a profiler to tell you where you have a non-interruptible block consuming your 16ms. The compiler can't know this a priori, unless you make significant restrictions to the type of code you write (no unbounded loops, for instance).
> Such toolkits are almost never thread safe because it's too much hassle to program a thread safe renderer and would make it too slow, so that browser-specific implementation constraint propagates all over the JS ecosystem.
From here on you seem to not be aware of the fantastic recent developments in WebWorkers, it is now quite easy to spin up worker threads in JS, which communicate with the main thread via message passing.