Skip to content

Conversation

@bigmistqke
Copy link
Owner

@bigmistqke bigmistqke commented Apr 8, 2023

Further performance-optimizations.
Test-scene is now Smileys, which has no blending-modes, but a more complicated structure (parent-siblings).

Optimizations include:

  • cascading transforms by rotating/translating the context before (and perform the inverse transformation after) render/hitTest instead of calculating DOMMatrix for each node seperately¹ .
  • default style.pointerEvents is now false, to prevent unnecessary hitTests. I was toying with the idea of Drag and Hover to set the style automatically, but decided against it. Drag and Hover with style.pointerEvents === false is now a no-op.
  • introduce internalContext.flags: controllers now can set flags. first use-case is Drag and flags.shouldHitTest: while dragging it sets flag.shouldHitTest to false, preventing jank while dragging².
  • a +/- 5% was spent in createLazyMemo of createControlledProps: this result is now memo-ed instead.
  • fix: Drag and Hover were merging style-props with each update, this is now done during initialization.

results experiment (ios m2 firefox, Smileys.tsx with 600 instances)

before

Screenshot 2023-04-08 at 20 52 29

after

Screenshot 2023-04-08 at 20 53 31

The part without red is when dragging, before and after are when hovering with the mouse. The hitTests clearly still have room for improvement.

¹ this does not account for skewing/scaling. ideally we should setTransform and calculate the matrix-values ourselves. This could possibly negate all performance-benefits, but then we could default to translate/rotate in case scale/skew no-ops
² It's a bit buggy atm: if you accidentally hover over something else just before you dragged that element will stay in that hover-style during the drag. Possible solution would be to introduce back the idea of having a focused, hovered and active element and checking for that inside Hover. This might even eliminate the idea of flags in favor of checking for the active element with a selector.

-ctx actions (ctx.translate, ctx.rotate) instead of updating DOMMatrix.

Note: this does not take into account skewing. further implementation
will use `ctx.setTransform` and manually calculating the values instead.
`pointerEvents` are now opt-in to prevent unnecessary hitTests
we still have to traverse the parent (and thus transform the ctx)
in case a sibling has a `pointerEvents: true`
fix group.hitTest to properly transform ctx before parentHood.
Controllers can now set internal flags. first usecase is `Drag()`:
when dragging we set `context.flags.shouldHitTest` to false
which short-circuits `shouldHitTest` in `createMouseEventHandler`
This eliminates jank that came apparent in bigger scenes:
`examples/Smileys` is able to drag now smoothly on ios/firefox (m2)
with 600 instances.
and we perform a check before each `hitTest`.
This basically short-circuits the `hitTest` once it encounters a hit
eliminating jank while for example hovering.

event-bubbling still is possible, but then it should be
explicitly set to true inside an event-handler set by the user
@bigmistqke
Copy link
Owner Author

bigmistqke commented Apr 8, 2023

Big performance-win by defaulting to propagation = false when having a hit in hitTest. Mouse-events can still bubble, but then event.propagation needs to be set to true by an eventhandler set by the user. Since it's logical to hit first elements which are on top of the layers, it prevents the hitTests to have to go all the way down the stack.

  • 600 Smileys with auto-render mode¹:

Screenshot 2023-04-08 at 21 29 20

  • 600 smileys in clock-mode:

Screenshot 2023-04-08 at 21 31 09

The jank (red lines) in clock-mode isn't noticeable, but still pretty epic auto-rendering out-performs clock-mode. Initially I envisioned to have auto-rendering as a first mode and if your scenes got too complex with too many animations and you needed more reliable rendering you would start using a clock to schedule rendering. But this graph showcases that simply throttling the render-effect is as performant, if not more performant.

¹ For clarity: in auto-render mode the render-function is executed inside a toggled createEffect. This way, whenever any prop changes that has an effect on rendering, and enough time has passed from last render, the render-function is automatically run. If nothing changed, nothing will re-render.

uses the more optimal `ctx.translate` and `ctx.rotate`
if no skew is present.

If skew is present, it will `ctx.getTransform` twice
mutate matrix1, mutate it, `ctx.setTransform(matrix1)`
and then revert it back to `ctx.setTransform(matrix2)`.

This is pretty wasteful, so there is room for improvement there.
Tokens with `style.pointerEvents: true` now register themselves.
Currently it's only being used to set the flag `hasInteractiveTokens`
which is being checked in `createMouseEventHandler`,
later iterations could use this information to only perform
hitTests on interactive tokens.
@bigmistqke
Copy link
Owner Author

bigmistqke commented Apr 8, 2023

this does not account for skewing/scaling. ideally we should setTransform and calculate the matrix-values ourselves. This could possibly negate all performance-benefits, but then we could default to translate/rotate in case scale/skew no-ops

Is implemented with transformedCallback.

Interactive tokens are now being registered at top-level :

Tokens with style.pointerEvents: true now register themselves. Currently it's only being used to set the flag hasInteractiveTokens which is being checked in createMouseEventHandler, later iterations could use this information to only perform hitTests on interactive tokens.

interactive tokens keep track of their transform-matrix
and `setTransform/resetTransform` the ctx.
@bigmistqke
Copy link
Owner Author

bigmistqke commented Apr 8, 2023

later iterations could use this information to only perform hitTests on interactive tokens.

is now implemented: Interactive tokens keep track of their transform-matrix during rendering and set/reset the transform-matrix of the context during hitTest. This way we don't need to go all the way up the tree to calculate the correct transformation-matrix for the element during hitTest, which allows us to only check for the tokens which have pointerEvents: true.

Screenshot 2023-04-08 at 23 57 56

More time is now spend for rendering instead of scripting: above run 30% of time was spend filling and stroking.

We could probably improve on this pattern by specifying the type of mouse-events the token listens to, to further eliminate unnecessary hitTests (Hover does not care about onMouseDown for example)

We do get from time to time some super-jank in the test with 600 Smileys, where it would be unresponsive for a second, so this is something to investigate further.

@bigmistqke bigmistqke temporarily deployed to github-pages April 22, 2023 14:08 — with GitHub Pages Inactive
@bigmistqke bigmistqke temporarily deployed to github-pages April 22, 2023 14:21 — with GitHub Pages Inactive
@tasteee
Copy link

tasteee commented Jun 30, 2025

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants