Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rewrite svg renderer #836

Merged
merged 78 commits into from
Oct 12, 2021
Merged

rewrite svg renderer #836

merged 78 commits into from
Oct 12, 2021

Conversation

pissang
Copy link
Contributor

@pissang pissang commented Oct 7, 2021

TLDR;

This PR rewrites our SVG renderer to provides better server-side rendering and brings a significant performance improvement.

What's the Problem Now

In our current implementation, there are two major issues:

  1. we render to SVG element from zrender graphic element directly. To avoid too much DOM manipulation, we add a lot of diff and cache strategies. But it seems it's still not enough and there are still lots of unnecessary DOM manipulation. But we rather keep the code simpler instead of doing more comparing and caching to improve the performance a bit. The reason of this choice is because the logic of converting from zrender element to SVG element is complex. Especially when it comes to shadow, clipPath, pattern, etc. These special effects are very different between SVG and Canvas. Doing cache can easily make the code very complex and buggy.

  2. We choose the Myers' Diff Algorithm to do elements diff. As described in fix: optimize arraydiff perf in first render and clear #832 . This algorithm will degenerate very fast when the elements changes lot. So like in first render, or elements are removed and added freqeuently. The performance becomes terrible.

How we Fixed it in This PR

At first this branch is not aiming to improve the performance of SVG renderer. It's for providing a new svg-ssr renderer. Which can do string-based SVG render and get rid of JSDom or node-canvas in the NodeJS environment. In this renderer we render all the elements to a VNode representation. Then convert these VNodes to SVG string. Each render is full and there is no cache or diff algorithm is used. Because of this, the code is simple and robust. Then I think we got VNode now. Why don't we use a virtual dom library and patch it to the container every frame? So I tried Snabbdom and the performance surprise me. The FPS is about 1 / 3 higher in the case of updating all elements in every frame. When it comes to the cases that elements are added or removed frequently. The FPS can be even 10x higher.

Here are two simple examples:

Before:
https://zrender-svg-ssr.glitch.me/svg.html

After:
https://zrender-svg-ssr.glitch.me/svg2.html

Why This Method is Fast

New implementation will render all elements into vnodes that use the following structure.

export type SVGVNodeAttrs = Record<string, string | number | undefined | boolean>
interface SVGVNode {
    tag: string,
    attrs: SVGVNodeAttrs,
    children?: SVGVNode[],
    text?: string

    // For patching
    elm?: Node
    key: string
}

This approach is fast because there is no DOM manipulation. Then patching algorithm from Snabbdom will render this vnodes into DOMs. Because mapping from vnode to DOM is very simple. The patching can diff all the attributes and only apply the changed attributes. The extra cost that new algorithm brings is it will create vnode for all elements in every frame. It will bring much pressure to the GC. But it turns out this extra cost is still worth it. The total render time is still much less.

What's New in This new Renderer

Because we will do full render to vndoes every frame now. The logic can be much simpler and robust. Turns out the new algorithm fixes some render bugs that we didn't notice. For example, shadows may be lost, text may not be removed, etc.

And we can now do server-side rendering with embedded CSS animation without need of JSDOM or node-canvas.

const zr = zrender.init(null, {
    renderer: 'svg',
    ssr: true
});
// add elements
zr.renderToString({
  cssAnimation: true
})

An example of rendered SVG

<svg width="510" height="510" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full">
<rect width="510" height="510" x="0" y="0" id="0" fill="none" fillOpacity="1"></rect>
<path d="M255 25.9A20 20 0 0 1 276.7 5.9A250 250 0 0 1 496.5 190.3A20 20 0 0 1 480.6 215.2L279.6 250.7A25 25 0 0 0 255 230Z" fill="#5470c6" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-0"></path>
<path d="M438.3 222.7A18 18 0 0 1 459.4 239A205 205 0 0 1 390.9 408.5A18 18 0 0 1 364.4 405.6L269.7 275.2A25 25 0 0 0 279.6 250.7Z" fill="#91cc75" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-1"></path>
<path d="M352.4 389.1A16 16 0 0 1 347.6 412.3A182.5 182.5 0 0 1 209.4 431.7A16 16 0 0 1 198.3 410.7L246.4 278.5A25 25 0 0 0 269.7 275.2Z" fill="#fac858" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-2"></path>
<path d="M205.3 391.6A14 14 0 0 1 186.1 399.4A160 160 0 0 1 113.1 328.9A14 14 0 0 1 120.3 309.4L231.8 264.4A25 25 0 0 0 246.4 278.5Z" fill="#ee6666" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-3"></path>
<path d="M144.4 299.7A12 12 0 0 1 128.4 291.9A131.9 131.9 0 0 1 125.2 231.5A12 12 0 0 1 140.3 222.1L231 248.1A25 25 0 0 0 231.8 264.4Z" fill="#73c0de" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-4"></path>
<path d="M160 227.7A10 10 0 0 1 153.4 214.4A109.4 109.4 0 0 1 171.5 184.4A10 10 0 0 1 186.3 183.9L237.6 237A25 25 0 0 0 231 248.1Z" fill="#3ba272" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-5"></path>
<path d="M196.6 194.5A8 8 0 0 1 197.3 182.7A92.5 92.5 0 0 1 215.3 171.5A8 8 0 0 1 226.2 176L246.4 231.5A25 25 0 0 0 237.6 237Z" fill="#fc8452" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-6"></path>
<path d="M229.3 184.5A6 6 0 0 1 233.4 176.7A81.3 81.3 0 0 1 248.5 174A6 6 0 0 1 255 180L255 230A25 25 0 0 0 246.4 231.5Z" fill="#9a60b4" stroke="white" stroke-width="4" stroke-linejoin="round" transform-origin="255px 255px" class="zr-cls-7"></path>
<style ><![CDATA[
.zr-cls-0 {
animation:zr-ani-0 0.5s cubic-bezier(0.33,1,0.68,1) 0.5s both;
}
.zr-cls-1 {
animation:zr-ani-1 0.5s cubic-bezier(0.33,1,0.68,1) 0.4375s both;
}
.zr-cls-2 {
animation:zr-ani-2 0.5s cubic-bezier(0.33,1,0.68,1) 0.375s both;
}
.zr-cls-3 {
animation:zr-ani-3 0.5s cubic-bezier(0.33,1,0.68,1) 0.3125s both;
}
.zr-cls-4 {
animation:zr-ani-4 0.5s cubic-bezier(0.33,1,0.68,1) 0.25s both;
}
.zr-cls-5 {
animation:zr-ani-5 0.5s cubic-bezier(0.33,1,0.68,1) 0.1875s both;
}
.zr-cls-6 {
animation:zr-ani-6 0.5s cubic-bezier(0.33,1,0.68,1) 0.125s both;
}
.zr-cls-7 {
animation:zr-ani-7 0.5s cubic-bezier(0.33,1,0.68,1) 0.0625s both;
}
@keyframes zr-ani-0 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-1 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-2 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-3 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-4 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-5 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-6 {
0% {
transform:scale(0,0);
}
100% {
}
}
@keyframes zr-ani-7 {
0% {
transform:scale(0,0);
}
100% {
}
}
]]>

</style>
</svg>

We can apply more optimizations to reduce the size of generated SVG. For example, put same attributes into a single class.

Other Changes?

Some compatible code for ancient browsers are removed:

  1. Adding event listeners for IE < 8 are removed.
  2. line dash support for IE <= 10 is removed.

Default measureText method can calculate text width without node-canvas. It's only accurate on default font. If developer is using other font that are very different on the metrics, it will be better to still use node-canvas

TypeScript is upgraded to 4.4

How to test this Branch in Apache ECharts

Checkout svg-ssr branch in echarts

plainheart and others added 30 commits September 1, 2021 19:44
…ox >= 39.

- To resolve the case where the chart uses SVG renderer and has shadow filter.
fix(type): improve gradient types
fix(svg): svg mouse event doesn't work normally in Firefox when using shadow.
fix: export Displayable for type annotation compatibility
fix prototype pollution in merge, clone, extend utilities
Prepare to release 5.2.1
fix: optimize arraydiff perf in first render and clear
src/svg-legacy/helper/PatternManager.ts Show resolved Hide resolved
src/core/util.ts Outdated Show resolved Hide resolved
src/svg/core.ts Show resolved Hide resolved
src/svg/helper.ts Outdated Show resolved Hide resolved
zrender Outdated Show resolved Hide resolved
src/svg/Painter.ts Outdated Show resolved Hide resolved
src/svg/Painter.ts Show resolved Hide resolved
src/svg/graphic.ts Outdated Show resolved Hide resolved
@pissang
Copy link
Contributor Author

pissang commented Oct 8, 2021

Added cssAnimation in renderToString parameters

Copy link
Member

@Ovilia Ovilia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks really promising to me! I can't wait to see it in future releases!

const res = /^([0-9]*?)px$/.exec(font);
const fontSize = +(res && res[1]) || DEFAULT_FONT_SIZE;
let width = 0;
if (font.indexOf('mono') >= 0) { // is monospace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may be fonts with 'mono' in name but are not monospace fonts, like Monoton. And it may be necessary to convert the name into lowercase before checking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem is, would font contains multiple font families like '12px Lato, Source Han Sans SC'? This may be especially helpful when developers want to set font families for multiple charsets, like in this case, Lato for Latin characters and Source Han Sans SC for Chinese.
If this is the case, then even if one of the font family is monotype doesn't necessarily mean that characters in other font families share the same width.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, is defaultMeasureText responsible for returning width that is as accurate as possible or just a rough result? If the answer is the later, maybe my previous considerations are not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only a very rough fallback in node environments that don't have any other methods to measure text.

In fact, we won't suggest developers using fonts that are not system-provided in the node server. If so, they need to use registerCanvas method and use node-canvas to get an accurate size.

Copy link
Contributor Author

@pissang pissang Oct 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure but will it be better if I do accurate check monospace instead of fuzzy font name? Just like serif and sans-serif.

// Generated from following code
//
// ctx.font = '12px sans-serif';
// const asciiRange = [32, 126];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it help if we also caches the width of '国' to represent Chinese character width? In the current implementation, fontSize is used as width in this case, which may not be necessarily accurate.

Copy link
Contributor Author

@pissang pissang Oct 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We assume Chinese characters all have the same width as fontSize here.

src/core/event.ts Show resolved Hide resolved
src/graphic/Text.ts Outdated Show resolved Hide resolved
Copy link
Member

@Ovilia Ovilia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks really promising to me! I can't wait to see it in future releases!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants