Vue do unecesarry re-computations

My code is quite big and complex, so I’ll just show a part of it with an explanation, and hope that provides enough information. The problem is this part of the code (part of a Vue component called ActWrapper):

		<div
			v-for="sceneWrapper in actWrapper.sceneWrappers"
			:key="sceneWrapper.id"
			class="scene-preview"
			:style="{
				left: `${actWrapper.offsetX + sceneWrapper.x}px`,
				top: `${actWrapper.offsetY + sceneWrapper.y}px`
			}"
			draggable="true"
			@dragstart="onSceneWrapperDragStart($event, sceneWrapper)"
		>
				<SceneComponent
					class="disabled-scene"
					:scene="sceneCodeToObject(sceneWrapper.code)"
				/>
		</div>

An ActWrapper consists of multiple SceneWrappers. The ActWrapper covers the entire screen, and works like a map the user can drag around (like in Google Maps). actWrapper.offsetX and actWrapper.offsetY keeps track of how much the user has dragged around the map.

On the map, the placement of SceneWrappers can be changed by dragging them around too. To keep track of where they are, their respective sceneWrapper.x and sceneWrapper.y are used.

Everything works, but dragging around the map is very lagy. I think that’s because the third last line above is re-executed (it executes some code provided by the user with eval(), so quite slow). It gets re-executed multiple times because actWrapper.offsetX and actWrapper.offsetY are frequently updated when the user drag around the map, but this part of the code is independent of actWrapper.offsetX and actWrapper.offsetY, so it really doesn’t need to be re-executed at all.

Any convenient solution to this problem? Or could the problem be something else? I’ve verified sceneCodeToObject() is called multiple times dragging around the map, and I’m guessing it’s because actWrapper.offsetX and actWrapper.offsetY are updated (don’t think anything else is updated when it lags).

I tried replacing actWrapper.offsetX and actWrapper.offsetY in the HTML code with 0, but sceneCodeToObject() is still called when dragging the map. Seems like the code is re-executed when actWrapper.offsetX and actWrapper.offsetY changes even though it’s not dependent on these two :S

I found:

I wonder if I have the same type of/similar problem. My ActWrapper component receives the actWrapper object as a props, so when I inside ActWrapper changes actWrapper, will that cause the entire ActWrapper to be re-rendered? Is the main problem here that I try to change a nested value in a prop?

I’m not exactly following but I think I understand enough to try to explain some of what you’re seeing.

The template will be compiled down to a render function. The render function behaves a bit like a computed property for VNodes. Vue doesn’t keep track of exactly how dependencies are used, it just knows whether they are used during rendering. If any of them changes, it’ll re-run the whole render function. Method calls like sceneCodeToObject will get called every time.

I don’t think you’ve mentioned what sceneCodeToObject returns but, given its name, I’d guess it returns a newly created object each time it’s called and that could be causing your lag. Even if the objects contain the same properties each time, Vue will see them as different when updating the scene prop. That will likely cause all of your SomeComponent components to re-render. While that might not include any actual DOM updates, a large amount of VNode manipulation could be enough to cause lag. Try putting an updated hook in SceneComponent to check whether it’s being re-rendered.

If that is what’s happening then you may want to consider some sort of caching within sceneCodeToObject. Maybe have a computed property that returns an object mapping the code to the object for all the items in actWrapper.sceneWrappers and then have sceneCodeToObject just return the correct entry from that object? That should give you canonical versions of those objects and avoid any unnecessary extra rendering, as well as avoiding the repeated calculation overhead.

One quirk of Vue 3 (if that’s relevant) is that it’ll re-render a component if any of its props changes, even if the prop isn’t used in rendering. In practice, that isn’t usually a problem because most prop changes do require a re-rendering. It also shouldn’t apply to nested properties within a prop object, so I’m not sure how relevant this is to your actWrapper prop.

1 Like

Ah, that explains it. I thought Vue was a bit smarter than this, but then it makes total sense.

I don’t think you’ve mentioned what sceneCodeToObject returns

It’s an object containing a scene, but what takes time is probably to execute the JS code pass to it using eval().

I’ve tried a computing property as you suggested, and that works great, thanks! But it would be nice to have a parameterized computed property, i.e.:

		scene(sceneWrapper){
			return sceneCodeToObject(sceneWrapper.code)
		}

The difference from a method is that the return value from this one would be cached (as usual in a computed property), so it’s not re-computed when the same parameters and reactive variables in the function are used. But that doesn’t exists in Vue, right?

To help with performance, use transform instead of position top/left. Using top/left will cause the browser recalc the layout and re-paint the viewport. More on that here: High Performance Animations - HTML5 Rocks

Highly suggest you take another approach over using eval, such as Function() which isn’t a security risk and is more performant.

2 Likes

Thanks for the tip!

Actually, I think I’m using new Function(), because I’m passing arguments to the code to be executed (don’t remember ^^’). But just to be clear, it can be risky to use new Function() too, right? It has access to global variables and can make changes to those?

About being more performant, I got curious and found this test:

https://www.measurethat.net/Benchmarks/Show/2858/0/eval-vs-new-function

It suggests new Function() is faster, but that one only measures execution time on new Function, not parsing time. A more fair comparison would be this:

https://www.measurethat.net/Benchmarks/Show/13441/0/eval-vs-new-function-without-cached-parsing

Which suggests eval() is faster. But I need to pass arguments to the code to be executed, so I’ll stick to new Function(). Thinking about it, it might not be the code execution that takes time, but compiling the JSX code JS code in it using babel. But anyway, using a computed property to cache the computations makes it fast enough.

Thanks for the help!

thanks for the information.

thanks for the awesome information.