Flutter?WebView性能優(yōu)化使h5像原生頁面一樣優(yōu)秀
引言
WebView 的文章分兩篇
- 在 Flutter 中使用 webview_flutter 4.0 | js 交互
- Flutter WebView 性能優(yōu)化,讓 h5 像原生頁面一樣優(yōu)秀(本文)
本篇和大家一起討論下性能優(yōu)化的問題。
WebView 頁面的體驗上之所以不如原生頁面,主要是因為原生頁面可以馬上顯示出頁面骨架,一下子就能看到內(nèi)容。WebView 需要先根據(jù) url 去加載 html,加載到 html 后才能加載 css ,css 加載完成后才能正常顯示頁面內(nèi)容,至少多出兩步網(wǎng)絡請求。有的頁面是用 js 渲染的,這樣時間會更長。要想讓 WebView 頁面能接近 Flutter 頁面的體驗,主要就是要省掉網(wǎng)絡請求的時間。
做優(yōu)化要考慮到很多方面,在成本與收益之間做平衡。如果不是新開項目,需要考慮項目當前的情況。下面分兩種情況討論一下。
服務端渲染
頁面 html 已經(jīng)在服務端拼接完成。只需要 html,css 就可以正常查看頁面(主要內(nèi)容不受影響)。如果你的項目的頁面是這樣的,那么我們已經(jīng)有了一個好的起點。
WebView 要顯示一個頁面,需要串行下面的過程。通過 url 加載到 html 后再加載 css,css 加載完成后顯示頁面。
url -> html -> css -> 顯示
我們可以對 css 的請求做一下優(yōu)化。優(yōu)化方案有兩種
- 內(nèi)聯(lián) css 到 html
- 把 css 緩存到本地。
第一種方案比較容易做,修改一下頁面的打包方案即可。很容易實現(xiàn)一份代碼打包出兩個頁面,一個外鏈 css ,一個內(nèi)聯(lián)css。但壞處也是很明顯的,每次都加載同樣的 css,會增加網(wǎng)絡傳輸,如果網(wǎng)絡不佳的話,對首屏時間可能會產(chǎn)生明顯的影響。就算拋開首屏時間,也會對用戶的流量造成浪費。
第二種方案可以解決 css 重復打包的問題。首先要考慮的問題是:css 放在本地的哪個地方?
css 放哪里
有兩個地方可以放
- 放在 asset,和 app 一起打包發(fā)布,好處是簡單可靠,壞處是不方便更新。
- 放在 文檔目錄,好處是可以隨時更新,壞處是邏輯上會復雜一些。
文檔目錄用于存儲只能由該應用訪問的文件,系統(tǒng)不會清除該目錄,只有在刪除應用時才會消失。
從技術(shù)上來說,這兩種方案都是可以的。先說下不方便更新的問題:既然 app 的其它頁面都不能隨便更新,為什么不能接受這個頁面的樣式不能隨便更新?如果是害怕版本沖突,那也好解決,發(fā)一次版,更新一次頁面地址,每個版本都有其對應的頁面地址,這樣就不會沖突了。根本原因是掌控的誘因,即使你能控制住誘因,你的老板也控制不住。所以還是老老實實選第二種方案吧。
放哪里的問題解決了,接下來要考慮的是如何更新 css 的問題。
更新 css
因為有可能 app 啟動后第一個展示的就是這個頁面,所以要在 app 啟動后第一時間就更新 css。但又有一個問題,每次啟動都更新同樣的內(nèi)容是在浪費流量。解決辦法是加一個配置,每次啟動后第一時間加載這個配置,通過配置信息來判斷要不要更新 css。
這個配置一定要很小,比如可以用二進制 01 表示true false,當然了可能不需要這么極端,用一個 map 就好。
如何利用本地 css 快速顯示頁面
在 app 上啟動一個本地 http server 提供 css。 我們可以在打包的時候把 css 的外鏈寫成本地 http,比如 http://localhost:8080/index.css
。
除了 css,頁面的重要圖片,字體等靜態(tài)資源也可以放在本地,只要加載到 html 就可以立即顯示頁面,省了一步需要串行的網(wǎng)絡請求。
到這里服務端渲染頁面的優(yōu)化就完成了,還是很簡單的吧,示例代碼在后面。
瀏覽器渲染
近年來,隨著 vue,react 的興起,由 js 在瀏覽器中拼接 html 逐漸成為主流。雖然可以用同構(gòu)的方案,但那樣會增加成本,除非必須,一般都是只在瀏覽器渲染??赡苣愕捻撁嬲沁@樣的。我們來分析一下。
WebView 要顯示一個頁面,需要串行下面的過程。通過 url 加載到 html 后再加載 css、js,js 請求完數(shù)據(jù)后才能顯示頁面。
url -> html -> css,js -> js 去加載數(shù)據(jù) -> 顯示
和服務端渲染的頁面相比,首次請求時間更長。多出了 js 加載數(shù)據(jù)的時間。除了要緩存 css,還要緩存 js 和數(shù)據(jù)。緩存 js 是必須的,緩存數(shù)據(jù)是可選的。好消息是 html 只有骨架,沒有內(nèi)容,可以連 html 也一起緩存。
緩存 js,html 的方案和緩存 css 的方案是一樣的。緩存數(shù)據(jù)會面臨數(shù)據(jù)更新的難題,所以只可以緩存少量不需要時時更新的少量重要數(shù)據(jù),不需要所有數(shù)據(jù)都緩存。app 的原生頁面也是需要加載數(shù)據(jù)的,也不是每種數(shù)據(jù)都要緩存。
數(shù)據(jù)更新之所以說是一個難題,是因為很多內(nèi)容數(shù)據(jù)是需要即時更新的。但數(shù)據(jù)已經(jīng)下發(fā)到客戶端,已經(jīng)緩存起來,客戶端不再發(fā)起新的請求,如何通知客戶端進行數(shù)據(jù)更新?雖然有輪詢,socket,服務端推送等方案可以嘗試,但開發(fā)成本都比較高,和獲得的收益相比,代價太大。
當緩存了 html,css,js 等靜態(tài)資源后,h5 就已經(jīng)和原生頁面站在同一起跑線上了,對于只讀的頁面,體驗上相差無幾。
加載數(shù)據(jù)后還有js 拼接 html 的時間,和加載的時間相比,只要硬件還可以的情況下,消耗的時間可以忽略
圖片不適合用緩存 css 的方案,因為圖片太大也太多。只能預加載少量最重要的圖片,其它大量圖片只能對二次加載做優(yōu)化,我們會在后面討論
瀏覽器渲染的頁面也需要打包的配合,需要把所有的要緩存的靜態(tài)資源地址都換成本地地址,這就要求發(fā)布的時候一份代碼需要發(fā)布兩個頁面。一個是給瀏覽器用的,資源都通過網(wǎng)絡加載。一個是給 WebView 用的,資源都從本地獲取。
思路已經(jīng)有了,具體實現(xiàn)就簡單了。下面我給出關鍵環(huán)節(jié)的示例代碼,供大家參考。
如何啟動本地server
本地不需要 https,用 http 用行了,但是需要在 AndroidManifest.xml 的 applictation 中做如下配置 android: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 負責處理靜態(tài)資源。
如果要兼容 windows 系統(tǒng),路徑需要用 path 插件的 join 方法拼接
如何讓 WebView 的頁面請求走本地服務
兩種方案:
- 打包的時候需要緩存的頁面的地址都改成本地地址
- 對頁面請求 在 WebView 中進行攔截,讓已經(jīng)緩存的頁面走本地 server。
相比之下,第 2 種方案都好一些??梢酝ㄟ^配置文件靈活修改哪些頁面需要緩存。
在下面的示例代碼中 ,cachedPagePaths
存儲著需要緩存的頁面的 path。
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; @override State<MyWebView> createState() => _MyWebViewState(); } class _MyWebViewState extends State<MyWebView> { late final WebViewController controller; @override 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(); } @override void didUpdateWidget(covariant MyWebView oldWidget) { if(oldWidget.url!=widget.url){ controller.loadRequest(Uri.parse(widget.url)); } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return Column( children: [Expanded(child: WebViewWidget(controller: controller))], ); } }
優(yōu)化圖片請求
如果頁面中有很多圖片,你會發(fā)現(xiàn),體驗上還是不如 Flutter 頁面,為什么呢?原來 Flutter Image Widget 使用了緩存,把請求到的圖片都緩存了起來。 要達到相同的體驗,h5 頁面也需要實現(xiàn)相同的緩存功能。
關于 Flutter 圖片請參見 快速掌握 Flutter 圖片開發(fā)核心技能
代碼實現(xiàn)
要如何實現(xiàn)呢?只需要兩步。
- 打包的時候需要把圖片的外鏈請求改成本地請求
- 本地 server 對圖片請求進行攔截,優(yōu)先讀緩存,沒有再去請求網(wǎng)絡。
第 1 條我舉個例子,比如圖片的地址為 https://juejin.com/logo.png
,打包的時候需要修改為 http://localhost:8080/logo.png
第 2 條的實現(xiàn)上,我們?nèi)€巧,借用 Flutter 中的 NetworkImage,NetworkImage 有緩存的功能。
下面給出完整示例代碼,貼到 main.dart 中就能運行。運行代碼后看到一段文字和一張圖片。
注意先安裝相關的插件,插件的名字 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}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { WebViewController? controller; @override 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')); }); } @override 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'); } } }
代碼邏輯
- 在本地文檔目錄的 www 文件夾中準備了一個 index.html 文件
- 啟動本地 server,通過訪問 http://localhost:8080/index.html 請求本地頁面。
- server 收到請求后,對圖片請求進行攔截,通過 NetworkImage 返回圖片。
第 2 條。本例中是直接訪問的 localhost,實際應用中,頁面地址是外鏈地址,通過攔截的方式請求本地。如何做頁面地址攔截前面已經(jīng)給出示例了。
第 3 條。打包后的時候?qū)λ袌D片地址都寫成了本地地址,改成本地地址的目的就是為了讓圖片請求都由本地 server 響應。本地 server 拿到 圖片地址后,再改回網(wǎng)絡地址,通過 NetworkImage 請求圖片。NetworkImage 會首先判斷有沒有緩存,有直接用,沒有就發(fā)起網(wǎng)絡請求,然后再緩存。
可能你覺得有點繞,既然最后還要用網(wǎng)絡地址,為什么還要先寫成本地地址,象攔截頁面請求那樣攔截圖片請求不香嗎?答案是不可以。兩個原因。
- webview_flutter 只能攔截頁面請求。
- 本地 server 不方便攔截 443 端口。
對比于攔截 443 端口,修改打包方案要容易的多。
關于圖片類型
在示例代碼中,用 RegExp( r'\.(png|image)$',)
判斷是否要響應請求。從正則可以看出,以 png 或 image 結(jié)果的圖片都能響應請求。判斷 image 是因為示例中的圖片地址是以 image 結(jié)尾的。
示例代碼只能支持 png 格式的圖片,示例圖片雖然是 image 結(jié)尾,但格式也是 png 格式。如果要支持更多格式的圖片,需要用到第三方庫。
關于圖片地址
如果圖片地址失改,可以自行換一個,隨使在網(wǎng)上找個 png 圖片 地址就行。
把圖片緩存到磁盤。
我們演示了把圖片緩存到內(nèi)存,當 app 被殺掉,緩存都沒了,除非緩存到磁盤。這項工作已經(jīng)有插件幫我們做了。 用 cached_network_image 替換 NetworkImage,稍加改動就可以實現(xiàn)磁盤緩存了。
總結(jié)一下
服務端染頁面方案
- 打包的時候需要打出兩個頁面,一個頁面的 css 外鏈接是外網(wǎng),一個頁面的 css 鏈接是本地。
- 在 App 啟動的時候根據(jù)配置信息預加載 css 存到文檔目錄。
- 啟動本地 server 響應 css 的請求。
瀏覽器渲染方案
- 打包的時候需要打出兩個頁面,一個頁面的 css,js 鏈接是外網(wǎng),一個頁面的 css,js 鏈接是本地。
- 在 App 啟動的時候根據(jù)配置信息預加載 html,css,js 存到文檔目錄。
- 根據(jù)配置信息攔截頁面請求,已經(jīng)緩存的頁面改走本地 server。
- 啟動本地 server 響應 html,css,js 的請求
圖片緩存
如果不做圖片緩存,通過前面兩個方案,h5 速度就已經(jīng)得到大大提高了。如果有余力,可以做圖片緩存。圖片緩存是可選的,是對前面兩種方案的加強。
- 給 app 用的頁面打包的時候把圖片地址換成本地地址。
- 啟動本地 server 響應圖片請求,有緩存就讀緩存,沒有緩存走網(wǎng)絡。
可能你的項目不同,有不同的方案,歡迎一起討論。
本文到這里就結(jié)束了,謝謝觀看。
番外
為了給自己一點壓力,上一篇 在 Flutter 中使用 webview_flutter 4.0 | js 交互 中我就預告說今天要發(fā)這篇性能優(yōu)化的文章。結(jié)果壓力是有的了,但卻沒能按時完工(理想情況是周日下午完工,這樣可以休息一下)。一個原因是 升級 flutter 報錯,浪費了一個上午,再有就是寫了一版后,并不滿意,又重寫了一版,最后才定稿。一直寫到深夜才把主要內(nèi)容寫完。早上起來又做了補充修改。
以上就是Flutter WebView性能優(yōu)化使h5像原生頁面一樣優(yōu)秀的詳細內(nèi)容,更多關于Flutter WebView頁面優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
微信小程序 獲取javascript 里的數(shù)據(jù)
這篇文章主要介紹了微信小程序 獲取javascript 里的數(shù)據(jù)的相關資料,這里通過實例來說明如何獲取javascript里的數(shù)據(jù),希望能幫助到大家,需要的朋友可以參考下2017-08-08微信小程序 時間格式化(util.formatTime(new Date))詳解
這篇文章主要介紹了微信小程序 時間格式化(util.formatTime(new Date))詳解的相關資料,這里附實例,一目了然很容易解決,需要的朋友可以參考下2016-11-11