This paper records a process of optimizing the load performance of a WebView-based visualization library of Flutter: Echarts_FLUTTER.

For any WebView-based component, HTML loading is an important aspect of performance. The basic principle of Echarts_FLUTTER is to render local Echarts charts with a WebView, so it is no exception.

The WebView load of echarts_FLUTTER mainly involves the following parts:

  • The HTML template
  • Echarts script
  • Echarts extension script
  • Chart logic code

The volume of template HTML and diagram logic code is small, with emphasis on echarts ontology and extended script loading.

One of the most powerful features of Echarts is that it has many powerful extensions, such as WebGL 3D charts and Map components. In today’s increasingly demanding data visualization, these extensions are almost as important as ontologies, so allowing users to easily introduce extensions is an essential feature. In addition, to avoid cumbersome asset management, we want both HTML and JavaScript scripts to be handled as strings, i.e. webViews load uniform resource locators (URIs).

So there are some problems:

  • Whether the loading time of the script is directly in THE HTML or inserted late
  • Uris have restrictions on some special characters and require secure encoding

Initially, the idea was that it would be best to load as much as possible in HTML in a single load as is generally understood. Given the large number of URI limiting characters in JavaScript scripts, the HTML is converted to Base64 encoding after being assembled. With no prior knowledge of the script introduced by the user, the encoding conversion is done dynamically with functions:

String _getHtml( String echartsScript, List<String> extensions, String extraScript, ) { ... // Concatenate and return all HTML and scripts} @override voidinitState() { super.initState(); // Base64 is converted at initialization time'data:text/html; base64,' + base64Encode(
      const Utf8Encoder().convert(_getHtml(
        echartsScript,
        widget.extensions ?? [],
        widget.extraScript ?? ' '))); _currentOption = widget.option; } @override Widget build(BuildContext context) {returnWebView(// Load everything initialUrl: _htmlBase64,...) ; }Copy the code

The performance test

For performance analysis, perform a simple preliminary performance test. The use case is to load three diagrams, the second of which introduces the WebGL rendering 3D diagram, and the third introduces the water balloon diagram that drives the drawing:

Using the CPU flame diagram in the Flutter Dev Tool, you can see the time footprint as follows:

Performance optimization

Echarts ontologies and many extended scripts are very large, and concatenating strings and encoding conversions at run time is undoubtedly time-consuming, but loading through URIs is necessary to be legal. How to resolve this conflict?

Instead of loading it all at once, insert the undefined, dynamic parts through the evaluateJavascript function without coding transformations; The determined, static transcoding is good for direct loading.

To do this, let’s do an experiment, all else being the same, just move all the scripts (Echarts ontologies, extensions) out of HTML and insert them with the evaluateJavascript function to see how the performance changes:

  @override
  void initState() {
    super.initState();
    _htmlBase64 = 'data:text/html; base64,'+ base64Encode(const Utf8Encoder().convert(_getHtml(// remove all scripts passed in to the echartsScript, // widget.extensions?? [], // widget.extraScript ??' '))); _currentOption = widget.option; } void init() async { final extensionsStr = this.widget.extensions.length > 0 ? this.widget.extensions.reduce( (value, element) => (value ??' ') + '\n' + (element ?? ' ')) :' '; await _controller? .evaluateJavascript(' '$echartsScript $extensionsStr const chart = echarts.init(document.getelementById ('))chart'), null); ${this.widget.extraScript} chart.setOption($_currentOption, true); '' ');
  }
Copy the code

The results are as follows:

As you can see, the loading part takes less time, and the onPageFinished function that contains the insert script takes more time, reducing the total time considerably.

It seems that encoding transformations for large strings are really cost-effective, and using the evaluateJavascript function for insertion is a viable direction.

Then we remove all the dynamic coding logic, and the template HTML is loaded directly as a regular string. And because HTML is now static and short, we can manually convert illegal characters and pass them directly into UTF-8 encoding, so our components don’t need to include the DART: Convert library and the source code is more intuitive.

const htmlUtf8 = 'data:text/html; UTF-8,
      
      
      
'
; @override void initState() { super.initState(); _currentOption = widget.option; } @override Widget build(BuildContext context) { returnWebView( initialUrl: htmlUtf8, ... ) ; }Copy the code

The test results are as follows:

As you can see, the time is further reduced, mainly in the loading part.

In this way, compared with the initial time, the performance improvement is still relatively large.

The script for the Echarts ontology is also deterministic, static, and what if it were put in HTML and encoded in advance:

const echartsHtmlBase64 = '... ';

  
  @override
  Widget build(BuildContext context) {
    returnWebView( initialUrl: echartsHtmlBase64, ... ) ; }Copy the code

The results are as follows:

Compared to the results of previous optimization, it takes more time.

You can see that “script in HTML” is not necessarily better than “evaluateJavascript function insert” and may even be more time-consuming due to coding and other reasons.

conclusion

To sum up, the final optimization was to load the template HTML as utF-8 URI strings and insert all the script and logic in the evaluateJavascript function.