Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance / View Caching potential #443

Open
rebo opened this issue May 6, 2020 · 0 comments
Open

Performance / View Caching potential #443

rebo opened this issue May 6, 2020 · 0 comments
Labels
enhancement New feature or request

Comments

@rebo
Copy link
Collaborator

rebo commented May 6, 2020

Although at some point a thorough look at all performance aspects of Seed will be useful (particularly vdom-diffing). I thought I would highlight some issues I have encountered whilst reviewing performance characteristics of my Seed Styling work.

I was particularly concerned about a potential performance penalty of rendering a large number of relatively expensive view functions. Therefore I began to first investigate this issue in current standard seed.

Baseline

To get a baseline, on standard seed-quickstart, I rendered the following view function : https://gist.github.com/rebo/6cf6611c59ef5bf79ba11d08784fc99c .

This creates a div! containing a h3!, p! , button! , EventHandler and significantly a md! element. The md! element renders markdown from a &'static str , and is therefore more involved than a simple wrapped html element.

I then render this 500 times by using:

fn view(_model: &Model) -> impl IntoNodes<Msg> {
    div![(0..500).map(|_| basic_styled_counter())]
}

Clicking the button in the view allows me to time an event loop, from Ev::Click to complete re-render. On my crappy old laptop this results in the following flame graph:

Screenshot 2020-05-06 at 01 13 46

The total render time is ~2.5 seconds, made up of 530ms vdom diffing and 2.02 secs view code. These figures are based on the debug build. I am principally using this mode for performance analysis because it is easier to determine what contributes to the overall timing because functions are labelled clearly in the browser's performance tools.

The highlighted timing of approx 3.78ms is the function seed::virtual_dom::node::Node<Msg>::from_markdown::hb78... which generates a seed Node from a markdown string.

it is clear that this is a major contribution to the view time therefore overall rendering performance. Indeed, 500 x 3.78 ÷ 100 = 1.89 seconds which is almost all of the view code cost. Therefore optimisation of expensive update/view code is important in order to maintain application responsiveness (ideally < 100ms).

The total render time in release mode is 813ms with approximately 158ms of that for vdom diffing. Release mode does help these figures but clearly not sufficiently to be less than 100ms for a re-render.

An almost 1 second delay on interactions is simply not responsive enough.

Obviously if one was in this position one might consider:

a) Is it reasonable to display 500 instances of a markdown render plus other elements?
b) Can the from_markdown function be better optimised?
c) Would it not be better to paginate and display a limited number of views?

All these points are very valid and sensible to consider. That said there may be occasions where one may have to render a significant number of relatively expensive functions.

In these circumstances it is worth considering what could reasonably be done to alleviate the performance cost.

View Caching

When Seed renders a view it enters the main view() and any descendent view functions building up a Node<Msg> tree. In this instance 500 'leafs' of the above view code. This Node tree is then passed to the vdom diffing algorithm. When a user interacts with the page, perhaps clicking the button in the above view function, the entire app's view is traversed again generating a completely new Node<Msg> tree. This occurs even though a button may only affect a single 'leaf' or sub-tree of Nodes.

Caching non-affected sub-trees should therefore significantly improve performance.

I have implemented a naive cache granulated at the view function level. Basically the view function can identify that it (and any child functions) need re-rendering. If it does not
then a cached copy of the Node<Msg> subtree is used and not re-generated.

Using this approach I was able to achieve the following timings in debug mode.

Debug Build:
caching     : view_timing: 432ms , vdom_diff: 588ms , total: 1.02s
non-caching : view_timing: 2.02s , vdom_diff: 530ms , total: 2.55s

Clearly view caching makes a substantial difference to the view timing, a reduction of around 75%. Vdom diffing is largely unaffected which is to be expected due to there being a similar number of nodes to diff.

In release build overall timing reduces from 1.02s to 193ms a reduction of 76%. vdom diffing is probably preventing the total render time from being less than 100ms. It appears vdom diffing is still around 156ms, and view code only 36ms!.

image

This 36 ms in release mode is down from 657ms in the release-non-caching version and a massive 2.02s in the non-caching in debug mode.

Clearly large savings can potentially be made using this approach.

Further work

I imagine that if specific Node<Msg> subtrees can be cached on the view side, then these could then either auto-key or somehow notify the vdom diffing algorithm to save on much of this work a well.

If similar gains can be made there then this example could indeed re-render in less than 100ms.

Note

I don't think this is massively concerning generally, it seems in this case a big cost is the markdown md! macro. Standard divs! etc render much much quicker. Most pages seem to render sub 20ms on release build which is of course perfectly fine!

relevant

https://www.toptal.com/react/optimizing-react-performance
#385

@MartinKavik MartinKavik added the enhancement New feature or request label May 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants