Performance in Halogen components

halogen

#1

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.

  1. 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.

  2. 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.

  3. 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.


PureScript design FAQ wiki
#2

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

#3

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.


#4

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?


#5

@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.


#6

Are you saying that in this code snippet that Control.Applicative.when will still evaluate the H.put?


#7

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.


#8

See Nate’s explanation in the ReadMe here: