How to Optimize Flutter Web and How Flutter Web work in Html Renderer
At present, there are few researches in Flutter Web, and this article will share that How to Optimize Flutter Web and How Flutter Web work in Html Renderer.
First of all, Flutter Web using the same framework as other Flutter platforms. Theoretically, most framework implementations are universal, But there are also some incompatible API, such as Canvas
. we will talk about this later.
At the moment, Flutter Web had two Renderer —html and canvaskit :
Html renderer is more lightweight, and basically depends on Web Platform’s API, using custom html tag like
<flt-*>
. But the problem with html is that it is depends on web platform, it lead to Flutter needs to do a lot of compatible logic on the Flutter Web.Canvaskit seems to fit the design logic of Flutter more better, because it using Skia + WebAssembly and this will improve performance. But using the wasm file brought by WebAssembly will lead to size become too big. In addition, WebAssembly is poor in compatibility, such as skia also needs to load its own font library.
By default, both html and canvaskit will be packaged in Web release, and then the canvaskit mode will be used on the PC side and the html mode will be used on the mobile side.
Of course, you can also specify the rendering mode through the configuration of flutter build web --web-renderer html --release
.
1. Building and Optimization
Although Flutter Web shares framework with other platforms, it has its own special engine implementation from Dart:
When Flutter web is packaged, the default code form
/flutter/bin/cache/lib/_engine
will becomes toflutter/bin/cache/flutter_web_sdk/lib/_engine
.
As shown in below, we can see that there will be different implementations such as html and canvaskit in the web SDK on the right.
Next, let’s start packaging the Flutter Web. As shown below, it is a simpl project of Flutter. After being deployed to the server, you can see that canvaskit is used for rendering after opening on the desktop, mainly including:
- 2.3 MB
main.dart.js
; - 2.8 MB
canvaskit.wasm
; - 1.5 MB
MaterialIcons-Regular.otf
; - 284 kB
CupertinoIcons.ttf
;
It can be seen that the size is unacceptable, because sample project is not large and the structure is not complex.
Therefore, we first consider to select one of the rendering Engine in html and canvaskit. I recommend using html renderer here, because we can easier to control it .
First you can see CupertinoIcons.ttf above, although it will be created through cupertino_icons
by default when Flutter project created, but since we don’t need to use it, it can be removed from the yaml.
After running flutter build web --release --web-renderer html
, you can see that the products loaded in HTML mode are very clean, and the way will to be optimized is now mainly in main.dart.js and MaterialIcons-Regular.otf .
Although we will use some vector icons of MaterialIcons-Regular.otf
in the project, it is obviously illogical to load a 1.5 MB font library file in full each time. So the command of --tree-shake-icons
provided in Flutter to help optimize this when packing.
Unfortunately, as shown in below, there will be bugs in this configuration under the current version 2.10. Fortunately, the shake-icons
behavior can be executed normally in the app platform.
So we can run flutter build apk
, and then use the following command to convert the MaterialIcons-Regular.otf
:
cp -r ./build/app/intermediates/flutter/release/flutter_assets/ ./build/web/assets
Now you can see that after optimization, MaterialIcons-Regular.otf
is only 3.2 KB.
To optimize main.dart.js, we will talk about deferred-component
. In Flutter, the lazy loading of Widgets can be realized by defining the Widget as deferred-components
, and this behavior will become multiple *part.js
after being compiled on Flutter web.
For example, first of all, we define an normal Flutter Widget.
import 'package:flutter/widgets.dart';
class DeferredBox extends StatelessWidget {
DeferredBox() {}
@override
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
Add import
by using keyword deferred as box
, and then box.loadLibrary()
to loads the Widgets, and finally through box.DeferredBox()
rendering.
import 'box.dart' deferred as box;
class MainPage extends StatefulWidget {
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: box.loadLibrary(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return box.DeferredBox();
}
return CircularProgressIndicator();
},
);
}
}
In this way, each page in the example is transformed into an independent lazy loading page, and then the part will be loaded when the page open. After the final packaging and deployment, it is shown in the following figure:
You can see the main.dart.js changed from 2.2 MB to 1.6 MB, But it is still not small, and there is not any way to optimization Directly.
Here, you can analyze this file through the source-map-explorer
from npmjs.com. First, add the --source-maps
during compilation, so that source map will be generated during packaging, and then execute source-map-explorer main.dart.js --no-border-checks
to generates the analysis chart:
Here only show the parts that can be mapped. We can see that 700k is almost the size of the whole framewok + engine + VM of Flutter web.
Is there anything else that can be optimized? There are still some external means, such as enabling gzip or brotli compression during deployment, as shown in below. After starting gzip, you can probably make main.dart.js drops to about 400k.
Summarized as follows:
- Remove useless icon references;
- Use
tree-shake-icons
; - Use
deferred-components
; - Open
gzip
;
Now you can try to publish your Flutter Web with better experience.
2.Html Renderer
The rendering of Flutter Web is very special in Flutter. As we said earlier that it comes with two rendering modes, and we know that in the design of Flutter, all Widget are drawn through Canvas
. If you look at the implementation of Canvas
in the framework at this time, you will find that it actually inherits NativeFieldWrapperClass1
:
NativeFieldWrapperClass1
mean That its logic is implemented by Engine of different platforms. The Canvas
code on Flutter web like this:
It can be seen that in the Canvas of Flutter web, whether to use CanvasKitCanvas
or SurfaceCanvas
will be judged according to logic. Compared with the CanvasKitCanvas
of skia, the SurfaceCanvas
closer to the Web platform will have higher coupling complexity.
First of all, as shown in below, it is the general structure of Canvas in Flutter Web. Next, we will mainly focus on SurfaceCanvas
. Why SurfaceCanvas
are so complex and how they are allocated and drawn.
Take a look at the example first, As shown in below. It can be seen that in HTML rendering mode, Flutter web has a lot of customized <flt-*>
tags to rendering, and in a long list, the tags will be controlled in an appropriate number to dynamically switch rendering when scrolling.
If we slow down to look at the details at this time, as shown in below, we can see that when the item is invisible, there is actually no content in <flt-picture>
, and when the item is visible, there will be a <canvas>
label under <flt-picture>
to draw the text.
Why is the text here drawn by <canvas>
instead of <p>
and so on? This is the SurfaceCanvas
rendering logic we focus on.
In the surfacecanvas of fluent web, text drawing generally occurs in this way. Basically, it starts from the picture and enters the drawing process:
In the SurfaceCanvas
of Flutter Web, text drawing generally occurs in this way. Basically, it starts from the picture and enters the drawing process:
As shown in the following key code, when hasArbitraryPaint
is true, it will enter the logic of BitmapCanvas
, otherwise DomCanvas
will be used.
void applyPaint(EngineCanvas? oldCanvas) {
if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) {
_applyBitmapPaint(oldCanvas);
} else {
_applyDomPaint(oldCanvas);
}
}
So here are two questions: what is the difference between BitmapCanvas
and DomCanvas
? What is the judgment logic of hasArbitraryPaint
?
1.First, the difference between BitmapCanvas
and DomCanvas
is:
DomCanvas
will draw by creating Html Element. For example, Text is rendered withp
+span
.BitmapCanvas
will give priority tocanvas
rendering. If the scene needs to be rendered with Html Element again.
2. In the web SDK, the parameter ofhasArbitraryPaint
is false by default, but it will be set to true when the following behaviors need to be performed. As show from these calls below, in fact, most of the painting logic will enter BitmapCanvas
first.
Text rendering in Flutter is generally realized through drawParagraph
, so theoretically, as long as there is text, it will enter the rendering process of BitmapCanvas
.
When does Flutter use canvas and p
+span
for Text in BitmapCanvas
?
Let’s first look at the following code. After running is shown in below. You can see that the text at this time is directly rendered by canvas.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
Next, add a red background to Container
. After running, you can see that the text becomes p
+span
, and the red background is realized through the draw-rect
. There is no canvas
. Why?
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
),
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
Here we need to talk about the drawRect
implementation of BitmapCanvas
first.
As shown in the following key code, when drawRect
is satisfied the function _useDomForRenderingFillAndStroke
, the rendering will be into buildDrawRectElement
, so the draw-rect
is used instead of canvas
.
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFillAndStroke(paint)) {
final html.HtmlElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
} else {
setUpPaint(paint, rect);
_canvasPool.drawRect(rect, paint.style);
tearDownPaint();
}
}
As shown in the following code, we can see that this function has many judgment conditions, and the condition to get true is to meet one of the three conditions. The meaning of each condition is roughly described in the following table.
bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) =>
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
((_childOverdraw ||
_renderStrategy.hasImageElements ||
_renderStrategy.hasParagraphs) &&
_canvasPool.isEmpty &&
paint.maskFilter == null &&
paint.shader == null);
The general process is also shown in the below. So there is no special configuration added when drawing the red background with Container
, so it will enter _drawElement
when drawRect
, we can see that BitmapCanvas
will adopt different drawing logic for different rendering scenes.
But why does more red background in front lead to the text becoming a label?
This is because if BitmapCanvas
is built with HtmlElement when _drawElement
, and then _closeCurrentCanvas
function will be called, this will set the_childOverdraw
true and cleared _canvasPool
.
So let’s look at the implementation of drawParagraph
. The code shown below can be seen When _childOverdraw
is true, the text will be drawn with Element
.
@override
void drawParagraph(EngineParagraph paragraph, ui.Offset offset) {
····
if (paragraph.drawOnCanvas && _childOverdraw == false &&
!_renderStrategy.isInsideSvgFilterTree) {
paragraph.paint(this, offset);
return;
}
····
final html.Element paragraphElement =
drawParagraphElement(paragraph, offset);
····
}
In BitmapCanvas
, three operations will be triggered _childOverdraw = true
and_canvasPool Empty
:
- _drawElement
- drawImage/drawImageRect
- drawParagraph
So now we can simply think that: without maskfilter (shadow) and shader (gradient), as long as the above three situations are triggered, HtmlElement drawing will be used.
Does it feel a little messy?
Not afraid. Let’s continue to look at the new example. Based on the original implementation of red background, shadow is added to the Container
to configure shadow. After running, you can see that both background color and text become canvas
rendering.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
Combined with the previous process, because there is a boxShadow
parameter, which will be converted into maskFilter
through the toPaint
method during painting, so in maskFilter != null
, the process will not enter the judgment of Element
, so canvas
is used.
Continuing with the previous example, if we add a ColorFiltered
control at this time, as mentioned in the previous table, when there is a ShaderMask or ColorFilter, the sInsideSvgFilterTree
parameter will be true. At this time, the rendering will directly use Element
to draw and ignore other conditions, such as BoxShadow
. The same is true from the running results.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: ColorFiltered(
colorFilter: ColorFilter.mode(Colors.yellow, BlendMode.hue),
child:Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
),
)
You can see UI will be drawing into draw-rect
andp
. why there is such logic? For example, Safari on iOS devices will not pass svg filter to canvas. If you continue to use canvas, it will fail to render normally, such as shader mask. more details: #27600 .
Continue with this example. If you do not add ColorFiltered
but add a transform
to the Container
, you can see the Widget drawing by draw-rect
and p
after running, because the transform
at this time belongs to TransformKind.complex
.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
transform: Matrix4.identity()..setEntry(3, 2, 0.001) ..rotateX(100)..rotateY(100),
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
Finally, let’s take another example. Here we return to the case where there is only red background and shadow.
Before that it uses the canvas
to render text because of its maskFilter != null
, now we configure TextDecoratoin
for Text
. After running, we can see that the background color is still canvas, but the Text
has becomep
.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
style: TextStyle(decoration: TextDecoration.lineThrough),
),
),
),
),
);
This is because as mentioned earlier, drawParagraph
has another judgment condition in this function_drawOnCanvas
: when drawing text on Flutter web, the text has TextDecoration
or fontFeatures
that are not none,_drawOnCanvas
will be set to fasle, which becomes the case of rendering with p
.
For example,
fontFeatures
are parameters that affect font selection. As shown in below, these behaviors are relatively troublesome to draw with Canvas on the web
But when will Domcanvas
be used?
Do you remember the methods listed above? You need got hasArbitraryPaint == false
if you want to entry _applyDomPaint
, such as no text, and then there is no shader ( gradient
) during drawRect
.
As in the previous example, draw a red box with shadow, but remove the text content.
After running, you can see that it is not canvas
but draw-rect
, because maskFilter != null
(with shadow) exits, but there is no text or shader(gradient
), so simple drawRect
will not trigger hasArbitraryPaint == true
, so it will directly use Domcanvas
to paint.
Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
),
),
),
)
finally, you can get the conclusion shown in below.
Combined with the examples introduced above, the process after entering bitmapcanvas can be summarized as follows:
结合前面介绍的例子,进入到 BitmapCanvas
之后的流程可以总结:
- If ShaderMask or ColorFilter exists, Element will be used.
- Generally ignored
_preserveImageData
also uses Element directly when there is complex matrix transformation, because the canvas support of complex matrix transformation is not good. - _After
_drawElement
, you will get_childOverdraw = true
and_canvasPool.isEmpty
. if without maskFilter and Shader, Element will be used
In addition, there are some special processing in drawParagraph
.
3.Additional
Although there are a lot of things introduced this time, the knowledge of Fluter Web in HTML rendering mode is far more than these. It is a good start to understand SurfaceCanvas
from a small perspective and from drawRect
and Text
.
In addition, you can see that there are many custom <flt-*>
tags in Flutter Web. These are through html.Element.tag('flt-canvas');
The corresponding relationship between them and the Flutter is shown in the following picture.
If you are interested about this, you can explore them in Chrome with dart_sdk.js
.