Halogen is a reasonably performant framework, but it’s easy to cause excessive computation and re-rendering in a component. As a rule of thumb, I like to follow a few guidelines (below), but I’m curious to hear other thoughts.
Remember that Halogen re-renders on state updates
Every time the state of your component changes, Halogen will re-run your render function and use its virtual DOM to diff changes against the current state of your UI in the browser. This loop is run whether you actually changed how your page should render or not. Be careful about excessively updating your component state and try to minimize the number of times you call modify or put.
Cache expensive computations in state, rather than re-run them each render
Some views might involve expensive computations. For example, a date picker may generate a month’s worth of dates and then format their display depending on the currently-selected date. If you simply calculate these dates in your render function, then — whether or not they needed to update — they’ll be recomputed from scratch every time your state updates. If you track which date is highlighted by the user then a simple mouse movement can be a real problem. A better option is to calculate the dates only when the input date changes, and in your render function just retrieve the dates from your component state.
Be careful with your receiver function
The receiver function allows you to listen to a stream of inputs from a parent component. Every time the parent component re-renders the receiver will run in the child component. If your receiver writes to your component state, then every re-render in the parent will re-render the child, too. This can cause excessive rendering. Depending on how expensive it is to re-render a child component and how many there are, this can cause performance problems.
I’d also say one should use CallByName.Applicative (when) rather than the normal one (i.e. Control.Applicative (when) since the latter evaluates let expressions whereas the former does not:
eval = case _ of
DoSomething next -> do
when someCondition \do
H.put newState
pure next
A Halogen component renders immediately after put or modify has been called, meaning:
eval (SomeAction next) = do
H.put someNewState -- component rerenders when this gets run
window >>= document >>= getElementById "someId" -- changes made by the previous line are visible here
pure next
Due to this, I tend to collect all the necessary data at the beginning of the eval function, do whatever calculations and effects I need and at the end update component state with a single call to put if necessary.
I still haven’t used or tested it, but I wrote this function the other day to handle state updates in receivers. From what I read here, my understanding of rerenders was correct, so it should make some sense:
patch ::
forall ri rs rt m t.
MonadState { | rs } m =>
RowToList rs t =>
EqRecord t rs =>
Nub rt rs =>
Union ri rs rt =>
{ | ri } ->
m { | rs }
patch i = do
s <- H.get
let ns = Record.merge i s
if ns /= s
then modify $ const ns
else pure s
@bklaric what do you use to collect all the changes?
@dariooddenino Plain get, followed by any HTTP requests and local storage reads the specific component needs. I haven’t been doing any checks to see if the read state and the new calculated state actually differ, if that’s what you’re asking.
It will allocate the put effect, however the Halogen runtime will not evaluate it. With CallByName.Applicative.when you are allocating a closure instead. Sometimes this can be a good tradeoff, sometimes not.
Components will not re-render if new state is referentially equal to previous state.
This feature is nice to use in child components if the input received from the parent is the entire state of the child. In the below block, the child will not re-render if the parent has not modified the input.
While still technically true, it’s better to just use a function returning HH.HTML wrapped in HH.lazy instead of a child component.
Manually prevent unnecessary state updates. (when appropriate)
If the component’s state will be considered updated by applying new identical data, (e.g. overwriting a record field to produce a new record reference), then an unnecessary re-render will be triggered. You can prevent this re-render by manually checking for differences before updating with H.modify or H.put. Note that comparisons are different for references versus values and types with an Eq instance.
handleAction = case _ of
HandleNewValue val -> do
old <- H.get
when (old.val /= val) $ H.modify_ _ { val = val }
HandleNewReference ref -> do
old <- H.get
unless (unsafeRefEq old.ref ref) $ H.modify_ _ { ref = ref }
Use HH.lazy and HH.memoized (when appropriate)
Avoid re-running functions that generate HH.HTML, if their arguments are the same as before. The Elm guide and this blog post provide a good explanation of how this works. Differences between HH.lazy and HH.memoized are that lazy compares references, while memoized lets you provide a custom comparison function. lazy also supports more than one function argument (multiple args to memoized may be wrapped in a record). More details on usage are in the Halogen source.
Use keyed elements for lists where items are inserted, removed, or reordered
Again, the Elm guide describes this well. See Halogen.HTML.Elements.Keyed for the PureScript equivalent. Note that using keyed elements may result in worse performance. For example, this keyed table performs much worse than the non-keyed equivalent.
Use browser debugger to check if render code is being run unnecessarily. It’s tough (or impossible) to detect renders in PS with logging, but you can find your component’s render code in the JS files and set a breakpoint there to catch any unnecessary re-renders. Edit:Debug.Trace is another option for tracking program flow.
Balance cost of checking for equality against cost of re-rendering.
Great summary and thanks for posting it! Just wanted to add a couple more comments.
You could at least detect the render function being called by inserting a debug statement.
render state = do
let _ = Debug.Trace.spy “rendering...” state
HH.div ... — ordinary render code
This is useful, but note that with memoized you can just stuff all your arguments into a record
memoized myEqFn ({ a, b } -> render a b) { a, b }
It’s worth noting that if the parent has not modified the input it sends to the child, then the input is never sent. So you don’t need to care about that case.
Edit: My comment was incorrect. Input is re-sent on each parent re-render.
I wouldn’t recommend relying on this as it’s not an intentional design decision, and it’s a bit magical. Reference equality is not something that is generally used in PureScript, so I’d suggest using an unsafeRefEq comparison as at least then it can be seen that the code is explicitly relying on it.
This also seems like a situation that will rarely be usable, since if the child component has no state other than that of the parent, it doesn’t really need to be a component - lazy / memoized could be used instead to control the “render boundary”. (I realise I suggested using the component in the other thread, but these functions should achieve the same effect).
I’d probably omit this sentence entirely - I think I know what it means (“reference types” sounds like Ref to me, but I guess you’re talking about types that can only be compared with unsafeRefEq? I don’t really know what the distinction between “value types” and types with an Eq instance would be though). Just knowing that the state update needs guarding with some kind of predicate, probably an equivalence relation, is sufficient I think.
Yeah , the “why” for this I mentioned in the other thread:
The same applies to the Input values, we can’t guard to see if they have changed between renders without any means of comparing them. I guess we could insert a magic reference eq check here, but I’d prefer to keep the need for the check explicit as it’s entirely predictable that way.
Here, here! I’ve actually also wondered if we could extend this refeq check to see if it’s the identity/get function just to avoid relying on this in any way .