Replies: 5 comments 10 replies
-
Thanks for the investigation. I will look into this in the next days. Maybe it could help to parse the Dom in the background or at least a part of it within an isolate via compute? |
Beta Was this translation helpful? Give feedback.
-
Thanks for the investigation @tneotia. I am absolutely not a Flutter expert, but most of all not when it comes to rendering. I do know that Flutter itself is good already at caching render elements (widgets to render), limiting rebuilding, etc. But yeah, if you have a (very) long 'page' of elements and no ListView-like lazy builder, it is hard to get it fast. As stated before and pretty much confirmed in your investigation, just trying to chop up the html in parts which can be lazily rendered via a ListView is not going to cut it, because of how generic html is. Individual use-cases can absolutely benefit from this but I can't think of any way to do this whilst still supporting the whole range of html cases we need to support. I expected the 'lazy parse html' to make a bit more of a difference, as parsing strings is something pretty slow, but it seems it did not make much of a difference? Browsers are fast, but unfortunately we are no browser, have limitations (being Flutter) to work with, need to support generic use-cases, and are not as smart as browser engineers. It will be hard to improve performance for such huge parts of html. In fact, being a Flutter library to render html into widgets, I am not even sure we should be so concerned about huge html rendering fast. But maybe I am looking too much as my personal use-case. |
Beta Was this translation helpful? Give feedback.
-
Can I be really obnoxious here and ask a tough question? If https://pub.dev/packages/flutter_widget_from_html is modularized, complete, fast, customizable... where does it leave us? What are our differences and strenghts? In general I like (as a library consumer) to have options, but if one is clearly superior (which I am not saying is true, but just starting the discussion)? |
Beta Was this translation helpful? Give feedback.
-
Thank you for this analysis. I think it confirmed what I kinda expected: jank is hard to fix, and this parsing/widget building is inherently slow. One of the biggest problems for us is that no matter what, we are parsing every time. Other libraries use stateful widgets will only be built once. We could look into moving into a stateful widget and then keeping a hash of the HTML and only re-rendering if the hash changes. We could also make a const constructor. Another thing we could do is attempt to make sure that we aren't allocating so often. Obviously, Dart isn't a language where we can fine-tune allocation, but some methods of recursion and how we call things recursively are faster than others. Plus, I'm sure that we are spending a lot of time in places where really simple optimizations will help out. For example, we have an "allow list" of elements which we iterate through each time. These sorts of lookups add up. We can also look into being more browser-esque on how we render HTML. A lot of browsers will notice that all is not good with certain HTML and will provide source -> source optimizations in order to either take out chunks of unused or invalid HTML, or correct mistakes such as putting table elements outside of a table tag. I don't think however that outside of images, it is really too smart to attempt to put anything into Isolates, or concurrently parse. That type of stuff is super complicated for very little benefit typically. The browser is blazing fast when it comes to parsing, and parsing isn't that big of an issue for speed. Actually building out the widgets, recursive descent, and the constant re-render of a stateless widget is what is likely causing the most jank. Lazy parsing sounds really cool, but again, something tells me that fine-tuning allocation, fine-tuning any lookups, and fine-tuning the number of times we render HTML will go a long way. To add to the conversation about why we are better than Sadly with the amount of school I took on, and Matt leaving (among other things) this library has remained stagnant. I'm very glad that @erickok and @tneotia have kept it going. One thing that I get when I read the code base of |
Beta Was this translation helpful? Give feedback.
-
So I ran some more profilers against our code, and used @tneotia 's sample HTML. I think that it gives a lot of context, but confirms everything that he said. The "flame graph" shows essentially microscopic amounts of time in our processes and shows a ton of work goes into painting/rendering. I don't think that any solution that attempts to make our HTML parsing or any of our internal code faster is warranted. Really, some sort of lazy rendering is needed. I think that a Stateful widget is justified to stop rebuilds and can help out a bit, but we really, really need to use a ListView of some sort to get this rendered. At the end of this post there's a discussion about what happens if there is a I'm not much of a Flutter guy, but I think that over the next couple of weeks I'm going to try and dig into competing libraries to see how they render a ListView, and take a look into the internals of ListView as it is my understanding (which is limited) that a ListView is just a nice abstraction over top of Slivers and that maybe there are some APIs that could help us in rendering these sub-lists. I think that if we can make our library competitive in performance that we'll be in a really awesome spot as we do have 1.1k stars and we are still very much being used. Our APIs continue to become more and more friendly, modularization is coming, and more and more features are being supported. |
Beta Was this translation helpful? Give feedback.
-
Hi all, this thread is to discuss my findings regarding the janky performance reported on #191 and #361.
I want to preface by saying I'm not a super advanced developer and to be honest, I don't understand much about how Flutter works under the hood. However I can see and feel the problem, and I want to try and help fix it, so here's what I found with some simple tests I did:
Note: It is not recommended to view this thread on mobile due to the 5000 line long HTML code that will not be collapsed into a dropdown.
Testing Methodology
Tested on a Galaxy S8, Android 9.0, debug mode.
Test HTML (5000 lines long)
I used the 'Flutter Performance' tab to check the scrolling FPS and track widget rebuilds. I also had 3 print statements - 1 for
example/main.dart
's build method, one for theHtml()
constructor build method, and one for theHtmlParser()
constructor build method.Testing
Current method
Scrolling FPS: ~10 FPS
Tons and tons of lag on initial widget build. The three build methods are called three times on initial build:
After this they are not called.
Using
final
to prevent widget rebuildScrolling FPS: ~10.5 FPS. Not much better
Still a ton of lag on initial widget build. The build methods are all called twice at the beginning. Afterwards they are not called.
Stateful widget - async build
Scrolling FPS: ~12 FPS
Profile mode: Avg frame render ~30ms, max 70ms (ideal is ~16ms for 60Hz), so we basically never hit 60 FPS even in release mode.
Not as much lag on initial build, but there is quite a bit of lag for a short period when switching between the
CircularProgressIndicator
and the built widget. The widget is only built once.Stateful Widget - synchronous build
Scrolling FPS: ~11 FPS
Still laggy on initial build just like all the others. The widget is only built once.
Stateful widget - synchronous ListView
The
cleanedTree
is parsed like so:Scrolling FPS: ~35-45 FPS
This is much much much better than any of the previous methods. There is still a slight bit of lag on initial widget build, but it is a vastly smoother experience once the listview is rendered.
Downsides: We need to make a better algorithm to split the items into a list. At the moment, it just splits all the elements immediately under
<body>
into a list. There's a bunch of extra cases:What if all the elements are wrapped in
<body>
><div>
or something similar? Currently just the div element would be put in a listview, which isn't what we want.What if we have elements that are supposed to be inline:
<body>
>Solve for <var>x<sub>n</sub></var>: log<sub>2</sub>(<var>x</var><sup>2</sup>+<var>n</var>) = 9<sup>3</sup>
? Currently these are rendered like block elements and not inline.What about any styling applied to
<body>
? Currently none of this styling takes effect.Etc, etc etc.
Comparison to flutter_widget_from_html
Synchronous build
Scrolling FPS: ~25 FPS.
Profile mode: avg frame render ~10ms, max 21ms (ideal is 16ms for 60Hz)
Definitely more performant than our current setup.
Async build
Scrolling FPS: ~23 FPS.
Both methods have a similar amount of lag on initial widget build, but their scrolling is noticeably smoother than our setup.
Conclusion
To say our performance is poor is an understatement. Pretty much every frame is "janky" with our current rendering, while flutter_widget_from_html rarely has jank.
The
ListView
looks pretty promising but it would be extremely hard to get right. We have got to find out a way to improve the rendering performance somehow.flutter_widget_from_html uses a combination of Stateful synchronous and Stateful async to load. See here, my code was based off how he did it. Great stuff.
After looking at the data, I believe most of his performance gains are not from caching or his tests, but rather just the algorithm he uses to render. Even with caching, we only had a ~1-2 FPS gain which could just be attributed to chance pretty much. His caching has a ~15 FPS gain over the quick one I threw together.
cc @erickok , @ryan-berger , and @DFelten
Beta Was this translation helpful? Give feedback.
All reactions