Flutter WebView performance optimization, making h5 as good as native pages

insert image description here

Hello everyone, I am 17.

The article of WebView is divided into two parts

  1. Use webview_flutter 4.0 | js interaction in Flutter
  2. Flutter WebView performance optimization, making h5 as good as native pages

This article discusses with you the issue of performance optimization.

The reason why the experience of the WebView page is not as good as that of the native page is mainly because the native page can immediately display the page skeleton and see the content at once. WebView needs to load html according to url first, then load css after loading html, and display page content normally after css loading is completed, at least two more steps of network request. Some pages are rendered with js, so the time will be longer. To make the WebView page close to the experience of the Flutter page, the main thing is to save the time of network requests.

To do optimization, many aspects must be considered, and a balance should be made between cost and benefit. If it is not a new project, the current situation of the project needs to be considered. The following two situations are discussed.

server-side rendering

The page html has been spliced ​​on the server side. Only html and css are required to view the page normally (the main content is not affected). If your project's page looks like this, then we've got a good starting point.

To display a page in WebView, the following process needs to be serialized. The css is loaded after the url is loaded into the html, and the page is displayed after the css is loaded.

url -> html -> css -> 显示

We can optimize the css request. There are two optimization schemes

  1. inline css to html
  2. Cache css locally.

The first solution is easier to do, just modify the packaging scheme of the page. It is very easy to realize that one code packs two pages, one external css and one inline css. But the disadvantages are also obvious. Loading the same css every time will increase network transmission. If the network is not good, it may have a significant impact on the first screen time. Even if you ignore the first screen time, it will waste the user's traffic.

The second solution can solve the problem of css repeated packaging. The first question to consider is: where is the css placed locally?

where to put css

There are two places to put

  1. Put it in the asset and package it with the app for release. The advantage is that it is simple and reliable, but the disadvantage is that it is inconvenient to update.
  2. The advantage of putting it in the document directory is that it can be updated at any time, but the disadvantage is that the logic will be more complicated.

The Documents directory is used to store files that can only be accessed by the app, the system does not clear this directory, and only disappears when the app is deleted.

Technically, both options are possible. Let me talk about the inconvenient update first: Since other pages of the app cannot be updated casually, why can't it be accepted that the style of this page cannot be updated casually? If you are afraid of version conflicts, it is easy to solve it. Post a version and update the page address once. Each version has its corresponding page address, so there will be no conflicts. The root cause is the temptation to control, and even if you can control the temptation, your boss can't. So honestly choose the second option.

The problem of where to put it is solved, the next thing to consider is how to update the css.

update css

Because it is possible that this page will be displayed first after the app starts, so the css should be updated as soon as the app starts. But there is another problem, updating the same content every time it starts is a waste of traffic. The solution is to add a configuration, load this configuration at the first time after each startup, and use the configuration information to judge whether to update the css.

This configuration must be very small. For example, binary 01 can be used to represent true and false. Of course, it may not be so extreme, just use a map.

How to use local css to quickly display the page

Start a local http server on the app to provide css. We can write the external link of css into local http when packaging, for example http://localhost:8080/index.css.

In addition to css, static resources such as important pictures and fonts of the page can also be placed locally. As long as they are loaded into html, the page can be displayed immediately, saving a step that requires serial network requests.

At this point, the optimization of the server-side rendering page is completed, it is still very simple, the sample code is in the back.

browser rendering

In recent years, with the rise of Vue and React, splicing HTML in the browser by js has gradually become the mainstream. Although an isomorphic solution can be used, it will increase the cost, and unless necessary, it is generally only rendered in the browser. Maybe your page is like this. Let's analyze it.

To display a page in WebView, the following process needs to be serialized. Load css and js after loading to html through url, and the page can only be displayed after js requests data.

url -> html -> css,js -> js 去加载数据 -> 显示

Compared with server-rendered pages, the first request time is longer. It takes more time for js to load data. In addition to caching css, js and data are also cached. Caching js is required, caching data is optional. The good news is that html has only a skeleton and no content, so even html can be cached together.

The scheme of caching js and html is the same as the scheme of caching css. Caching data will face the problem of data updating, so only a small amount of important data that does not need to be updated from time to time can be cached, and all data does not need to be cached. The app's native page also needs to load data, and not every kind of data needs to be cached.

Data update is a difficult problem because many content data need to be updated in real time. But the data has been delivered to the client and cached, and the client no longer initiates new requests. How to notify the client to update the data? Although there are polling, socket, server-side push and other solutions that can be tried, the development costs are relatively high, and compared with the benefits obtained, the cost is too high.

After caching static resources such as html, css, js, etc., h5 is already on the same starting line as native pages. For read-only pages, the experience is almost the same.

After loading the data, there is still time for js to splice html. Compared with the loading time, as long as the hardware is still available, the time consumed can be ignored

Pictures are not suitable for caching css solutions, because the pictures are too large and too many. Only a small number of the most important images can be preloaded, and a large number of other images can only be optimized for secondary loading, which we will discuss later

The pages rendered by the browser also need to be packaged, and all the static resource addresses to be cached need to be replaced with local addresses, which requires that two pages need to be published for one code when publishing. One is for the browser, and resources are loaded through the network. One is for WebView, all resources are obtained locally.

The idea is already there, and the implementation is simple. Below I give the sample code of key links for your reference.

How to start the local server

You don't need https locally, you can use http, but you need to do the following configuration in the applictation of AndroidManifest.xmlandroid:usesCleartextTraffic="true"

import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
import 'package:path_provider/path_provider.dart';

Future<void> initServer(webRoot) async {
    
    
  var documentDirectory = await getApplicationDocumentsDirectory();
  
  var handler =
      createStaticHandler('${
      
      documentDirectory.path}/$webRoot', defaultDocument: 'index.html');
  io.serve(handler, 'localhost', 8080);
}

createStaticHandler is responsible for handling static resources.

If you want to be compatible with the windows system, the path needs to be spliced ​​with the join method of the path plugin

How to make the WebView page request go to the local service

Two options:

  1. When packaging, the addresses of the pages that need to be cached are changed to local addresses
  2. Intercept the page request in WebView, and let the cached page go to the local server.

In contrast, the second option is better. You can flexibly modify which pages need to be cached through configuration files.

In the sample code below, cachedPagePathsthe path of the page that needs to be cached is stored.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class MyWebView extends StatefulWidget {
    
    
  const MyWebView({
    
    super.key, required this.url, this.cachedPagePaths = const []});
  final String url;
  final List<String> cachedPagePaths;

  
  State<MyWebView> createState() => _MyWebViewState();
}

class _MyWebViewState extends State<MyWebView> {
    
    
  late final WebViewController controller;

  
  void initState() {
    
    
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(NavigationDelegate(
        onNavigationRequest: (request) async {
    
    
          var uri = Uri.parse(request.url);
          // TODO: 还应该判断下 host
          if (widget.cachedPagePaths.contains(uri.path)) {
    
    
            var url = 'http://localhost:8080/${
      
      uri.path}';
            Future.microtask(() {
    
    
              controller.loadRequest(Uri.parse(url));
            });
            return NavigationDecision.prevent;
          } else {
    
    
            return NavigationDecision.navigate;
          }
        },
      ))
      ..loadRequest(Uri.parse(widget.url));
    super.initState();
  }
  
  void didUpdateWidget(covariant MyWebView oldWidget) {
    
    
    
    if(oldWidget.url!=widget.url){
    
    
      controller.loadRequest(Uri.parse(widget.url));
    }
    
    super.didUpdateWidget(oldWidget);
  }
  
  Widget build(BuildContext context) {
    
     
    return Column(
      children: [Expanded(child: WebViewWidget(controller: controller))],
    );
  }
}

Optimize image requests

If there are many pictures on the page, you will find that the experience is still not as good as the Flutter page, why? It turns out that the Flutter Image Widget uses caching to cache all the requested images. To achieve the same experience, the h5 page also needs to implement the same caching function.

For Flutter images, please refer to Quickly master the core skills of Flutter image development

Code

How to achieve it? It only takes two steps.

  1. When packaging, you need to change the external link request of the picture to a local request
  2. The local server intercepts the image request, reads the cache first, and does not request the network again.

Article 1 Let me give you an example. For example, the address of the picture is https://juejin.com/logo.png, which needs to be changed tohttp://localhost:8080/logo.png

In the implementation of Article 2, we took a trick and borrowed NetworkImage from Flutter. NetworkImage has a caching function.

The complete sample code is given below, and it can be run after pasting it in main.dart. After running the code, you can see a piece of text and a picture.

Note that the relevant plug-ins are installed first, and the name of the plug-in is included in import.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
import 'dart:ui' as ui;
import 'package:webview_flutter/webview_flutter.dart';

const htmlString = '''
<!DOCTYPE html>
<head>
<title>webview demo | IAM17</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, 
  maximum-scale=1.0, user-scalable=no,viewport-fit=cover" />
<style>
*{
  margin:0;
  padding:0;
}
body{
   background:#BBDFFC;  
   text-align:center;
   color:#C45F84;
   font-size:20px;
}
img{width:90%;}
p{margin:30px 0;}
</style>
</head>
<html>
<body>
<p>大家好,我是 17</p>
<img src='http://localhost:8080/tos-cn-i-k3u1fbpfcp/
c6208b50f419481283fcca8c44a2e3af~tplv-k3u1fbpfcp-watermark.image'/>
</body>
</html>
''';
void main() async {
    
    
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
    
    
  const MyApp({
    
    super.key});
  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
    
    
  WebViewController? controller;
  
  void initState() {
    
    
    init();
    super.initState();
  }

  init() async {
    
    
    var server = Server17(remoteHost: 'p6-juejin.byteimg.com');
    await server.init();

    var filePath = '${
      
      server.webRoot}/index.html';
    var indexFile = File(filePath);
    await indexFile.writeAsString(htmlString);
    setState(() {
    
    
      controller = WebViewController()
        ..loadRequest(Uri.parse('http://localhost:${
      
      server.port}/index.html'));
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
        home: Scaffold(
      body: SafeArea(
        child: controller == null
            ? Container()
            : WebViewWidget(controller: controller!),
      ),
    ));
  }
}

class Server17 {
    
    
  Server17(
      {
    
    this.remoteSchema = 'https',
      required this.remoteHost,
      this.port = 8080,
      this.webFolder = 'www'});
  final String remoteSchema;
  final String remoteHost;

  final int port;
  final String webFolder;
  String? _webRoot;
  String get webRoot {
    
    
    if (_webRoot == null) throw Exception('请在初始化后读取');
    return _webRoot!;
  }

  init() async {
    
    
    var documentDirectory = await getApplicationDocumentsDirectory();
    _webRoot = '${
      
      documentDirectory.path}/$webFolder';
    await _createDir(_webRoot!);
    var handler = Cascade()
        .add(getImageHandler)
        .add(createStaticHandler(_webRoot!, defaultDocument: 'index.html'))
        .handler;

    io.serve(handler, InternetAddress.loopbackIPv4, port);
  }

  _createDir(String path) async {
    
    
    var dir = Directory(path);
    var exist = dir.existsSync();
    if (exist) {
    
    
      return;
    }
    await dir.create();
  }

  Future<Uint8List?> loadImage(String url) async {
    
    
    Completer<ui.Image> completer = Completer<ui.Image>();
    ImageStreamListener? listener;
    ImageStream stream = NetworkImage(url).resolve(ImageConfiguration.empty);
    listener = ImageStreamListener((ImageInfo frame, bool sync) {
    
    
      final ui.Image image = frame.image;
      completer.complete(image);
      if (listener != null) {
    
    
        stream.removeListener(listener);
      }
    });
    stream.addListener(listener);
    var uiImage = await completer.future;
    var pngBytes = await uiImage.toByteData(format: ui.ImageByteFormat.png);
    if (pngBytes != null) {
    
    
      return pngBytes.buffer.asUint8List();
    }
    return null;
  }

  FutureOr<Response> getImageHandler(Request request) async {
    
    
    if (RegExp(
      r'\.(png|image)$',
    ).hasMatch(request.url.path)) {
    
    
      var url = '$remoteSchema://$remoteHost/${
      
      request.url.path}';
      var imageData = await loadImage(url);
      //TODO: 如果 imageData 为空,改成错误图片
      return Response.ok(imageData);
    } else {
    
    
      return Response.notFound('next');
    }
  }
}

code logic

  1. Prepared an index.html file in the www folder of the local documentation directory
  2. Start the local server and request the local page by visiting http://localhost:8080/index.html.
  3. After receiving the request, the server intercepts the image request and returns the image through NetworkImage.

Article 2. In this example, localhost is directly accessed. In actual application, the page address is an external link address, and the local host is requested by interception. An example of how to do page address interception has been given earlier.

Article 3. After packaging, all image addresses are written as local addresses. The purpose of changing them to local addresses is to make image requests be responded by the local server. After the local server gets the image address, it changes back to the network address and requests the image through NetworkImage. NetworkImage will first judge whether there is a cache, use it directly, and initiate a network request if it is not, and then cache it.

Maybe you feel a bit convoluted. Since the network address is still used in the end, why do you need to write the local address first? Isn’t it good to intercept image requests like intercepting page requests? The answer is no. Two reasons.

  1. webview_flutter can only intercept page requests.
  2. It is inconvenient for the local server to intercept port 443.

Compared to blocking port 443, it is much easier to modify the packaging scheme.

About image type

In the sample code, use RegExp( r'\.(png|image)$',)to determine whether to respond to the request. As can be seen from the regex, images that result in png or image can respond to the request. The image is judged because the image address in the example ends with image.

The sample code can only support images in png format. Although the sample image ends with image, the format is also in png format. If you want to support pictures in more formats, you need to use a third-party library.

About the picture address

If the image address is lost, you can change it by yourself, just find a png image address on the Internet.

Cache the image to disk.

We demonstrated caching images to memory. When the app is killed, the cache is gone, unless it is cached to disk. There are already plugins that do this for us.
Replace NetworkImage with cached_network_image, and disk caching can be implemented with a little modification.

in conclusion

Server-side dyeing page scheme

  1. When packaging, you need to print two pages, one page's css external link is the external network, and the other page's css link is local.
  2. When the App starts, preload the css according to the configuration information and save it to the document directory.
  3. Start the local server to respond to the css request.

Browser Rendering Scheme

  1. When packaging, you need to print two pages, one page's css, js link is the external network, and the other page's css, js link is local.
  2. When the app starts, preload html, css, and js according to the configuration information and save them to the document directory.
  3. Intercept page requests according to the configuration information, and change the cached pages to the local server.
  4. Start the local server to respond to html, css, js requests

image cache

If you do not do image caching, the speed of h5 has been greatly improved through the previous two solutions. If you have spare capacity, you can do image caching. Image caching is optional and is an enhancement to the previous two schemes.

  1. When packaging the pages used by the app, replace the image address with a local address.
  2. Start the local server to respond to image requests, read the cache if there is a cache, and go to the network if there is no cache.

Maybe your project is different, there are different solutions, welcome to discuss together.

This article is over here, thank you for watching.

extra

In order to give myself a little pressure, in the last article using webview_flutter 4.0 | js interaction in Flutter , I announced that I would post this performance optimization article today. As a result, the pressure was there, but the deadline was not met (ideally on a Sunday afternoon so you could take a break). One reason is that I wasted a morning when I upgraded flutter and reported an error. Another reason was that I was not satisfied after writing a version, so I rewrote another version before finalizing it. I didn't finish writing the main content until late at night. Woke up in the morning and made supplementary revisions.

Due to time constraints, if there is something wrong, please be gentle and correct.

Guess you like

Origin blog.csdn.net/m0_55635384/article/details/129004460