fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
28.31k stars 3.46k forks source link

[Question]: loadFromJSON unusably slow with larger data #9632

Open rfricz opened 6 months ago

rfricz commented 6 months ago

CheckList

Version

6.0.0-beta18

In What environments are you experiencing the problem?

Chrome

Node Version (if applicable)

None

Link To Reproduction

https://codesandbox.io/p/sandbox/fabric-vanillajs-sandbox-forked-hn7flc

Steps To Reproduce

  1. Run the preview in the provided codesandbox
  2. A 1.8MB large JSON file (data.json) is loaded, it contains 3150 objects, roughly half of them Rect, half Textbox (there's no animation etc.)
    • The file was originally created by fabric using toObject without any performance issues, the problem only arises when loading it back to a clean canvas
  3. It takes minutes to process, with Chrome asking multiple times whether to exit the page or wait because it's unresponsive

Expected Behavior

I'd expect this to take a few seconds max. There is a similar 1.4MB large data2.json file in the codesandbox, which loads almost instantly in my actual app.

Actual Behavior

After minutes of waiting and countless prompts about killing the page, it eventually succeeds, but it's a really bad user experience.

Error Message & Stack Trace

No response

ShaMan123 commented 6 months ago

This is another cases that fabric's caching mechanism causes more harm than good

Add this line before loading the json

fabric.FabricObject.ownDefaults.objectCaching = false;
ShaMan123 commented 6 months ago

https://codesandbox.io/p/devbox/cjfmny?file=%2Fsrc%2Findex.ts%3A8%2C36

ShaMan123 commented 6 months ago

you will face another issue I want to fix which is to optimize on screen checking Currently it iterates over all the objects each mouse move/render

rfricz commented 6 months ago

Thanks, disabling caching fixes it. Are there any downsides to this?

What's interesting is that the performance problem only occurs in Chrome – when I do it in Firefox, it loads the big file just fine without the fix. Not sure why the Chrome JS engine struggles with it.

Also another thing seems slow with big files – drawing object borders on hover and then redrawing on mouse:out. You can see it at https://codesandbox.io/p/devbox/fabric-vanillajs-sandbox-forked-yh5yph. Is there a better way to code this? I haven't found anything better than requestRenderAll on mouse:out, is there a way to just redraw one object but without its borders? Setting the border color to white and doing the same as on mouse:over draws the borders in grey for some reason.

ShaMan123 commented 6 months ago

I haven't looked into it but I am guessing it is to do with looping through all the objects Once we optimize that you would probably not feel the lag. You can try yourself. Hold an array of objects that are on screen and update them instead of letting fabric iterate over all of them each render tick checking isOnScreen.

rfricz commented 6 months ago

How do I update (re-render) just one object in a way that removes its already rendered selection borders?

asturur commented 6 months ago

@rfricz which is the value of your machine window.devicePixelRatio?

I think we are missing the real issue here. The app is configured to run on a 10.000+ pixel canvas.

image

If you have an hidpi screen you go easily to 20.000 pixels that is out of spec and i m not even sure how chrome is trying to work at that resolution.

Without looking into extra renders or skip offscreen calculations, disabling retina scaling in your application makes it load very quickly.

Then we can talk of how to best understand the values for

for your application.

asturur commented 6 months ago

This is another cases that fabric's caching mechanism causes more harm than good

Add this line before loading the json

fabric.FabricObject.ownDefaults.objectCaching = false;

With the standard basic config, disabled retina caching, an so with caching on and offscreen calculation, even with all the objects that could live easily without caching, the app is smooth on my machine. It is probably using way more memory than it needs.

While objectCaching is a very simplistic way to save on calculation and is memory intensive and is not optimized, i would argue it rarely makes more harm than good.

asturur commented 6 months ago

@rfricz also consider that you can't extend the canvas size at will. At some point it just crashes, and is not fabricJS fault is the browser limitation.

If you are working on a tabloid style app and you need to output a jpeg of it, you chose the hardest way. you should look into having a canvas that is as big as the screen at maximum and then use the viewport to move up and down to it, in that way you can extend how much you want on the screen, but you will face the same exact issue when going to output a file for it.

If you are not planning to output a png/jpeg export for it i do not know why are you using fabricjs.

If you share more detail on what is the goal of your software we can tell you which is the smoothest way to use fabricJS for it

asturur commented 6 months ago

on my machine with caching off a render takes 25ms off target for 60fps

image

with caching on it takes 17ms, is a bit blurrier and still off target for 60fps

image

with cachinig on and a smaller canvas, that is taking advantage of skipping offscreen objects, i go down to 8ms, with no saving in memory because we are not deleting the cache for objects that are detected offscreen

image

Unfortunately for some weird reason i m not able anymore to capture memory used, but i m sure with caching the ram used is a lot

rfricz commented 6 months ago

Guys, thanks for looking into it. The app's goal is to create custom apartment buzzer name plates that can be printed and saved & loaded to/from files for future updates. I needed drawing of rects & curves with submillimeter precision, interactive textboxes and ability to add, scale & move uploaded images. Fabric was the only thing that had it all so I went for it, you can test the app at https://domtel.cz/jmenovky/ if you're interested.

To print the stuff, I fill an iframe with the result of canvas.toSVG(), so no jpeg/png is involved. It works well with no margins and 100% scale set in the print dialog to ensure precision to mm.

I have a 4K monitor and disabling retina scaling makes the performance issue go away, but it looks terribly pixelated like on an ancient computer, so I can't use that. The large json spanned 10 A4 pages, which probably won't ever be needed, so I'm not too worried about it. I like the idea with the screen-sized viewport, but how can I make the Fabric canvas scrollable? Is there any example of that? Thanks.

asturur commented 6 months ago

you can connect a mousewheel event to it, you can draw fake scrollbars if needed, but i would stay away from it. You can also just organize in pages and have multiple canvases if you are going to print on separate pages.

i can try to make a scrollable example, is a lot i don't need one.

I understand you have a large 4k screen, but again, which is your window.devicePixelRatio on your browser?

rfricz commented 6 months ago

Sorry I forgot, my window.devicePixelRatio is 1.5. Fake scrollbars don't sound like a good idea to me and multiple canvases would make everything too complicated. Is there a way to have native scrollbars on the canvas? I can have a wrapper div with native scrollbars, but that won't solve the perf issue, right?

asturur commented 5 months ago

1.5 so not as high as 2, but you should get out of the 16384 pixels at around 11.000 causing the performance issue at that point. Multiple canvases are not a big deal imho, but if you really want to keep everything in one you could try to size a div large than necessary and architect something with the canvas in a fixed position and a div scroll event that changes your viewport transform.

I never did that on my own, so i do not know if is super complicated. Could be a nice example

zhe-he commented 5 months ago

Sorry I forgot, my window.devicePixelRatio is 1.5. Fake scrollbars don't sound like a good idea to me and multiple canvases would make everything too complicated. Is there a way to have native scrollbars on the canvas? I can have a wrapper div with native scrollbars, but that won't solve the perf issue, right?

Coincidentally, I've written the requirement for scrolling the canvas. You can use it directly.

https://github.com/fabricjs/fabric.js/assets/11306583/86d7e93a-bc24-492c-876a-414deaba56b4

const SCALE = {
    MAX: 256,
    MIN: 0.02,
};
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);

const makeMouseWheel = (canvas) => (options) => {
    const e = options.e;
    e.preventDefault();

    const isTouchScale = Math.floor(e.deltaY) != Math.ceil(e.deltaY);
    // const isTouchScale = e.wheelDeltaY === -3 * e.deltaY;

    if (e.ctrlKey || e.metaKey) {
        const speed = isMac
            ? isTouchScale
                ? 0.99 // mac Touch panel
                : 0.998 // mac Command + Mousewheel
            : isTouchScale
              ? 0.99 // window Touch panel
              : 0.998; // window ctrl + Mousewheel
        let zoom = canvas.getZoom();
        zoom *= speed ** e.deltaY;
        if (zoom > SCALE.MAX) zoom = SCALE.MAX;
        else if (zoom < SCALE.MIN) zoom = SCALE.MIN;
        canvas.zoomToPoint(options.viewportPoint, zoom);
        return;
    }
    const vpt = canvas.viewportTransform.slice(0);
    vpt[4] -= e.deltaX;
    vpt[5] -= e.deltaY;
    canvas.setViewportTransform(vpt);
};

// use
const mouseWheel = makeMouseWheel(canvas);
canvas.on("mouse:wheel", mouseWheel);
// canvas.off("mouse:wheel", mouseWheel);
rfricz commented 4 months ago

Thanks for the tip, the mousewhell panning works well. The problem is that it creates an “infinite” canvas without scrollbars.

zhe-he commented 4 months ago

Hmm... This requires a separate scrollbar configuration. I've noticed that the need for a scrollbar has been mentioned in other issues as well. Plan A is to use a div to simulate the scrollbar, and Plan B is to use a canvas to draw a virtual scrollbar. For the scrollbar logic, let's take the horizontal direction as an example. We need to calculate the distance A between the leftmost and rightmost edges of all the graphics in the canvas, and the distance B between the left and right edges of the canvas that the current user can see. The ratio B/A is the size of the scrollbar. The position of the leftmost shape is denoted as C, and the position of the rightmost shape is denoted as D. The current leftmost position of the canvas is denoted as E, and the current rightmost position of the canvas is denoted as F. The percentage of the position where the scrollbar is located is CE / (CD - EF).

asturur commented 4 months ago

i used fake scrollbars on canvas for a while, it wasn't that bad.