Approaching React performance with Halogen

I’m using a small app to explore different Halogen techniques for achieving good performance, and I’m wondering what I need to do to approach performance of the React equivalent.

The test app displays which row of a table is being hovered over. I’m using 1k rows in the table to more easily detect needless re-rendering of the table. Bootstrap css is used to highlight the active row (although highlighting doesn’t work for keyed Halogen in Firefox).

The Halogen attempts are here:

The React equivalent is here:

The Halogen versions are laggy and the biggest issue is a frequent 3-second pause for garbage collection.

What techniques should I explore to improve performance of the Halogen versions of this app? Are there ways to avoid needless re-rendering of this table, while still allowing the table contents to be stored in component state? Is there a memo equivalent in Halogen? Do I need to break my Halogen app into separate components? I thought single components are recommended. I think a major gap in my understanding of Halogen is if and how state is diffed automatically.

I’ll repost takeaways from this troubleshooting discussion to the main Halogen Performance thread.

1 Like

I’m not sure what I’m doing differently, but even with basic I don’t see any discernible lagginess or GC pausing. With 10k rows it’s not exactly snappy (and it’s horrible with profiling enabled), but I still don’t get the GC issue.

I’m not sure what you mean about state diffing… I don’t think many things do that, React certainly doesn’t. The diffing occurs when updating the DOM.

Since your list of rows is static you could avoid re-rendering the vdom for them by computing them as a value, that should reduce GC overhead, but I don’t really know if that’s the problem since I don’t know why its behaving the way you describe for you.

I’m not sure where you read that single components are recommended. There are performance benefits to using components since vdom diffing doesn’t cross component boundaries (which is an improvement over React actually). You wouldn’t make components pervasively for the smallest thing, but that’s just because it’s inconvenient. I generally use a component anywhere I have a reusable element with non trivial functionality (so, not a button, but perhaps a panel with several buttons in it), or anywhere that would make me want to reach for a lens to deal with state modifications in the absence of a component. Items in a list are a prime example of that, unless they are purely being rendered.

edit: there are indeed options for memoized rendering outside of component usage too: https://github.com/purescript-halogen/purescript-halogen/blob/v5.0.0-rc.8/src/Halogen/HTML.purs#L68-L115

2 Likes

Thanks for testing on your system, and sharing tips. I’ll do some more investigation on my end.

It seems like React.PureComponent does state diffing. I notice with Halogen that if I update the new state to match the previous state, then a re-render will still be triggered. The halogen guide shows an example of using when (oldN /= n) $ H.put n, but I wonder if this is a capability that could/should be built-into components. Wrapping render with memoized eq (as you linked below) seems like a good option.

My full use case involves modifying the list content too, but I eliminated that to simplify this experiment.

I probably misinterpreted these two pieces of advice as performance-based, rather than developer-convenience-based:

To confirm, would you use a Halogen component for each todoRow in this React todo-app?

For what it’s worth, I also am not seeing the GC pause in Chrome or Firefox – for example, this is my output of a performance recording highlighting a bunch of rows, pausing, then highlighting a bunch more rows.

1 Like

Ah right, I wouldn’t have described that as state diffing, since it’s not comparing state values any more deeply than “is it different?”.

The when situation for state modification is there as a means of preventing unnecessary rendering based on the component receiving an input, yeah. Without explicitly guarding here the child will re-render any time a parent re-renders, negating one of the benefits of using components. This guarding doesn’t happen automatically as we didn’t want to impose an Eq constraint on component state, as that is far too restrictive in general.

This is because every modify in halogen will cause a render (unless the state is referentially equal to the previous value… this is a hack to prevent re-rendering on get also, due to the way MonadState is defined, rather than something we specifically wanted)

I guess I should emphasise that distinction in the Halogen docs, since yeah it’s pretty much the opposite of what you thought :smile: - before lazy and memo using components was the only way of taking control over situations like this.

It’s right on the threshold of where I’d make a component, I could go either way. I think maybe no in this specific case, because there’s a need to see the overall state of completeness of the items and it’s convenient to do that having all the item state accessible, but if they had one more interaction/piece of modifiable state each, then I’d probably switch them to components.

For your example, one thing that might be worth trying is not making each item into a component, but making the whole list part a child component. You could still even populate it dynamically with entries passed down as an input to that component (with the aforementioned guarded update in the list component’s receiver). This should improve things because the only diffing that will take place then is for the vdom specifically rendered by the outer component, where the text changes. The list entries would be isolated from that diff because of the component boundary, which should speed things up greatly both in avoiding rendering vdom nodes for the items, and then not having to diff them again, which is still an overhead even in my suggestion of making the rows static.

I have a dumb question… Why not separate state modification from rendering, so that the end-user can modify state without also rendering? If we want the “old” behavior, one could use functions that do rendering immediately after modifications to state such as putR below:

v1 <- H.get
H.put (v1 + 2) -- no rendering occurs here
H.render -- rerender triggered here

putR (v1 + 3)
where
 putR v = H.put v *> H.render

I’m guessing there’s a good reason for not doing this this way that I simply don’t know yet.

I worked with Forest at CitizenNet writing Ocelot (the YouTube video), so I can confirm that his comment is specifically to do with React’s “everything is a component!” philosophy and developer convenience, not performance. At the time it was more common for people to try to copy React patterns directly into Halogen, like making the smallest things (like buttons) components. In Halogen this gets pretty annoying, and it’s not necessary.

But there’s also a difference in terminology; what you would call a pure function returning HTML in Halogen you might call a function component or a ‘pure, functional component’ in React; but no one would call this function a ‘component’ in Halogen.

I think Gary and I are agreeing on this – if you can’t just pass in an input and an action to raise on click or something like that, if the reusable element really does need its own internal state that’s annoying or complicated to push up to a parent, then do the component. But it shouldn’t be the first impulse. If you can do a function, do that. Even better with Hooks!

React uses reference equality in some places to try and improve performance, but it’s a bit dodgy (see the warning in the link you provided). Halogen actually also does this: it checks reference equality on the state object itself before re-rendering. For that reason you actually shouldn’t see any work done if you update the new state to match the previous state, like this:

do
  state <- H.get
  H.put state

For this reason I don’t think it’s necessary to guard the state update in the Halogen guide (wouldn’t this pass an unsafeRefEq check? @garyb). This optimization can actually cause its own confusion: Text input not tracking state, but it’s necessary due to how MonadState is implemented in PureScript – without this check, calls to H.get would cause re-renders.

The advice to use H.memoized eq is good; it avoids the pitfalls of React’s PureComponent for complex data. The H.lazy functions are also quite nice – there’s a great article about them (written for Elm but which applies equally to PureScript):

If you’re doing things in a Hooks component you can still use memoized, and there’s also the same useMemo hook as exists in React if you’re memoizing particular values in the body of the hook definition.

Just be careful that checking equality with eq isn’t even more costly than just re-rendering! You can also pass a custom equality function that just checks on things like whether the UUID is the same to save on performance.

1 Like

There’s no technical reason we couldn’t separate state modification and render, but making that easy seems like a footgun to me: it’s almost never the case that you want a state modification to skip rendering.

It wouldn’t help in the receive case discussed here either, as all it would do is change the when from determining whether the state needs updating to whether the component needs rendering instead, with the same predicate.

The only times I can think of where I’ve modified state and not needed a corresponding render to go with it would be when storing things like fork or subscription IDs. And realistically, usually when I do that it’s also flipping a loading flag or something to show a loading state in the component, so the render would be needed anyway.

The lazy and memoize functions for the HTML rendering are much more useful a means of dealing with this than having an explicit render, as they can apply to granular chunks of the vdom, versus just controlling whether the whole component re-renders. They preserve the modify/render setup as being declarative rather than imperative too, which given the preferences that led us all PS should seem like a definite win. :wink:

In cases like these where you have state but don’t want changes to the value to cause renders you can store the value in a ref in state. That way updates to the value skip rendering.

This wouldn’t be that useful for something like fork and subscription ids because it’s unlikely they cause any performance issue. But if you store say a scroll position or something you may not want every change to cause a render.

@thomashoneyman As a note for your hooks implementation. Since the component function is always invoked (due to the dependency on input), you might want to provide a way to short-circuit that in the implementations receiver. That way it can avoid invoking the component function at all in the case the the input does not change.

@natefaubion Yes, here I need to add a way to prevent the state update unless the input has changed:

…though I don’t want to force an Eq instance and it would be irritating to require an input -> input -> Boolean equality function. I may be able to use reference equality, though I haven’t yet checked.

I don’t see how that would avoid invoking the component function, though, as this is only after that function has already been called. Am I misunderstanding you?

Yeah, that’s a good option too.

Although for this specific example I think I probably wouldn’t store the scroll position in the state if I wasn’t using it during rendering, and instead would just read it at the time I needed it.

Right, good point, no need to store something like that in state if it isn’t used for rendering. And I suppose if you are using it for rendering then you can’t really get around sticking it in state directly because otherwise it’s not available to the render function (you can’t read the ref in the render code). So scroll state isn’t a good example. But something else, like a debouncer, makes sense to put in mutable storage in state to save on unnecessary state updates.

I think you are misunderstanding me slightly. When a hook component is “compiled”, there must be a receiver, which upon receiving an input must invoke the hook function, correct? I’m saying there should be a way to bail out here, so that in the receiver, it doesn’t invoke the hook function at all if the new input is equal to the previous. React does this with an HOC https://reactjs.org/docs/react-api.html#reactmemo

I see – I thought you meant the Hooks.component \input -> ... function, but you mean the Hooks function that gets run within the resulting component when new input is received. Yes, I can skip running any Hooks functions when the input is unchanged (and let a user guard with their own equality function if they want to). That could either be baked in to the component function or applied to the resulting component. Either way it’s worth doing.

This! That todo app does it more as a demonstration than out of need. Usually it’s just faster (and the code is simpler) to naively re-render until you experience a problem.

1 Like

Thanks for all these great pieces of advice. It led to a lot of good investigations.

I gave that a try here, and it worked really well! About 5ms event response times compared to ~50ms with the earlier unnecessary re-rendering.

It’s also interesting to note that if the parent component passes complete state to the child (e.g. child state is just the entries array), then the built-in referential equality check works automatically. For example, all of these skip the re-render for untouched entries passed by the parent:

handleAction = case _ of
  HandleInput entries ->
    H.put entries
    -- H.put $ entries <> []
    -- H.put $ identity entries

Here are some examples where the entries data remains equivalent, but the reference changes, so a new render is triggered:

H.put $ sort entries -- assuming previously sorted
H.put $ filter (const true) entries
H.put $ map identity entries

I was then curious to see what techniques would work if the entries array passed by parent was a field of the child state. Checking with unsafeRefEq seems to work well. Full example here:

HandleInput entries -> do
  old <- H.get
  unless (unsafeRefEq old.entries entries) $ H.put { entries }

Another troubleshooting task was figuring out whether the render function was being called in unnecessary situations. I didn’t know how to do this in PS, since logging isn’t allowed in the render function, but JS breakpoints work fine, as shown in the screenshot below:

It turns out that a restart was enough to fix my original 3-second GC lag issue, but I’m glad it inspired a deeper investigation into achieving the best performance in Halogen.

1 Like

As Thomas mentioned in the other thread, you can use spy / trace / etc. from Debug.Trace to log in pure code during development.

Just wanted to check what you mean by checking if rendering was unnecessary though - you mean trying to determine if the state is being modified unnecessarily? As just to reiterate, component rendering only happens:

  1. during component initialization, after the initialState has been computed, before the initializer runs
  2. when the state is modified

:slightly_smiling_face:

Trying to determine if halogen thinks the state was modified, even if I know the state was not modified in a meaningful way. Monitoring re-renders was just an indirect way to to investigate what causes references to change.

As of Hooks v0.2.1 this is now possible.