Memory leak in React when using HTML5 Canvas - javascript

I have a project in my React app that draws to the canvas.
I'm invoking it in my component like:
function CanvasProject() {
const canvasRef = useRef(null);
const cleanup = useRef(null);
useEffect(() => {
var c = canvasRef.current;
const cleanup.current = render(c);
}, [])
return(
<canvas style={{width: "100%"}} ref={canvasRef}>
Your browser does not support the HTML5 canvas tag. Stop using Netscape.
</canvas>
)
}
The render function gets the "2d" context from the canvas a few times, gets image data with ctx.createImageData, and sets image data with ctx.putImageData.
render() also adds an onclick handler to the canvas, but I've tried commenting out that code and the memory leak is still present.
As for the memory leak itself, when I go back and forth between routes on my App, it starts to slow down the page. I'm going back and forth between routes that respectively do and do not have my component.
I've tried examining the memory usage with Firefox dev tools, and here's the memory profile when I first start the app:
Followed by the memory usage after I go back and forth between a few routes:
Obviously without seeing the full code I'm not expecting a "your bug is here" answer, but are there any things I should consider about the way canvas memory management works? Are there any other dev tools I could use to find out the source of the large chunks of Array and ArrayBuffer data that is being generated? Basically looking for any leads and a sanity check, to make sure I'm not missing something obvious.

Can't be positive as I do not have a lot of experience with canvas, however, if the render method creates listeners then they will likely persist after the component has unmounted.
Within the useEffect hook you could try adding a function to cleanup when the component dismounts. The most likely option I could think of is change the cleanup.current to null.
useEffect(() => {
var c = canvasRef.current;
const cleanup.current = render(c);
return () => {
cleanup.current = null;
}
}, [])

Related

Reactivity when element added to DOM

I want to integrate qr-scanner to my project. This library is capable of decoding QR-Codes by utilizing the camera. It just needs the reference to a html-video-element and then renders the webcam-stream and optionally some indicators of a QR-code being found to this element.
The easiest component would be something like this:
import { useRef } from "react";
import QrScanner from "qr-scanner";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
return (
<video ref={videoElement} />
)
}
however, qr-scanner checks, if the passed target-element is already part of the DOM:
if (!document.body.contains(video)) {
document.body.appendChild(video);
shouldHideVideo = true;
}
the video-element will never be added to the DOM, when the QrScanner-object is created. This leads to shouldHideVideo being set to true, which disables the video altogether later in the library-code.
So I think I need some kind of way to react to the video-element being added to the DOM. I thougt about using a MutationObserver (and tried it out by stealing the hook from this page), however I only wanted to print out all mutations using the hook like this:
import { useRef, useCallback } from "react";
import QrScanner from "qr-scanner";
import { useMutationObservable } from "./useMutationObservable";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
const onMutation = useCallback((mutations) => console.log(mutations), [])
useMutationObservable(document, onMutation)
return (
<video ref={videoElement} />
)
}
however, I never got a single line printed, so to me it seems, as if there are no mutations there.
Did I maybe misunderstand something? How can I react to the video-element being added to the document?
Ok, so I think I didn't provide enough information, but I found it out on my own:
The MutationObserver doesn't work, because I told it to watch document, yet I'm actually inside of a shadowdom with this particular component! So I suspect that mutations to the shadowdom won't be detected.
Also, because I'm inside of a shadowdom, document.contains(videoElem.current) will never be true. So in this particular case I have no better choice, than to copy the code of qr-scanner into my project-tree and adapt as needed.
On another note: useLayoutEffect is a react-hook, that is scheduled to run after DOM-mutations. So that's what I would use if I had to start over with this idea.

ReactJs (native) - Transition variable from one value to another incrementally

Is there a way to make a transitional change of value with increments over time in React native (or any method in JS that I don't know of / can't find?
I could of course create some sort of loop with interval or setTimeout, but I was wondering if there is something like the Animated API with interpolation that could be used to just change a value when there is not actually any view that is being animated?
In my case I want to change the screen brightness of the users device smoothly, right now it goes from 0-1 instantly, which is not what I want.
I will provide code in case anyone would like to see that.
This process takes place in a useEffect() that reacts when the user is opening a scannable code on their screen.
const [brightness, setBrightness] = useState(null);
...
...
useEffect(() => {
(async () => {
const currentBrightness =
await DeviceBrightness.getBrightnessLevel();
setBrightness(currentBrightness);
})();
if (isOpened) {
DeviceBrightness.setBrightnessLevel(1);
}
if (!isOpened && brightness) {
DeviceBrightness.setBrightnessLevel(brightness);
}
}, [isOpened]);

How to setState in a onTick event that ticks every millisecond

I'm trying to set a state in an onTick event for a clock.
<Viewer>
<Clock
startTime={start.clone()}
stopTime={stop.clone()}
currentTime={start.clone()}
multiplier={50}
onTick={_.throttle(handleValue, 1000)} // this thing ticks every millisecond
/>
<Entity
ref={ref} // here is the ref to get the value I want to set state with
position={positionProperty}
tracked
selected
model={{ uri: model, minimumPixelSize: 100, maximumScale: 100.0 }}
availability={
new TimeIntervalCollection([
new TimeInterval({ start: start, stop: stop }),
])
}
/>
</Viewer>
Here is the handleValue function.
const handleValue = (clock) => {
//setting the state here ( I want to display the chaning the value over time)
setHeadingValue(ref.current.cesiumElement._properties._heading.getValue(clock.currentTime));
}
};
The problem is it looks like it tries to re-render over and over which freezes the app.
Due to the nature of setState, this behavior makes sense. But I feel like there is an answer that's escaping me.
May I have some insight as to what I could do? I'm out of ideas.
I'm using Resium ( a react library) and right now I'm setting the value using .getElementByID() and appending to the dom.. which defeats using react in the first place...
Here is a code sandbox: https://codesandbox.io/s/resium-cesium-context-forked-bpjuw?file=/src/ViewerComponent.js
Some elements are not showing because we need a token, but that does not affect the functionality I'm looking for. Just open the console of the code sandbox and go to the ViewerComponent.js
thank you for your help
As I see, the problem here not in the library, but in a way of managing calculation and visualization.
As few people already mentioned, for UI, user don't need more than 60fps, but for process sometimes we need more.
So the solution is to separate processing from visualization.
To be more generic, here is an example in pure JavaScript:
// Here it may be some component
const clockView = document.getElementById('speedyClock')
let state = null
// This function mimicking dispatch on state update
function setState(val) {
state = val
clockView.innerHTML = state
}
const FPS = 30
// Any state out of visualization scope
// Not the React state!!!
let history = []
let nextUiUpdate = Date.now()+(1000/FPS)
/// Super speedy process
setInterval(function() {
history.push(Math.random())
const now = Date.now()
// Update according to visual frame rate
if(now >= nextUiUpdate) {
// Prepare visual updates
setState(JSON.stringify({count: history.length, history}, null, 2))
history = []
nextUiUpdate = Date.now()+(1000/FPS)
}
}, 1)
<pre id="speedyClock"></pre>
I would suggest using requestAnimationFrame, instead of setInterval as their is no point rerendering the React tree more than once every screen refresh.
By using a combination of shouldComponentUpdate and setState you can effectively setState and prevent unnecessary renders.
This is my codesandbox url to show it.
You can access your wanted value as often as you want, but we need to prevent 1 render per millisecond with shouldComponentUpdate function.

Use GraphQL data in gatsby-browser?

I have an app with some route ID's (basically a bunch of sections in a long SPA) that I have defined manually. I fetch these in gatsby-browser.js and use them in conjunction with shouldUpdateScroll, checking if the route ID exist, and in that case, scroll to the position of the route/section.
Example:
export const shouldUpdateScroll = ({ routerProps: { location } }) => {
const container = document.querySelector('.site')
const { pathname } = location
const projectRoutes = [`project1`, `project2`]
if (projectRoutes.indexOf(pathname) !== -1) {
const target = document.getElementById(pathname)
container.scrollTop = target.offsetTop;
}
return false
}
This works well for my usecase.
Now I want to add something similar for a page where the content is dynamically created (fetched from Sanity). From what I understand I cannot use GraphQL in gatsby-browser.js, so what is the best way to get the ID's from Sanity to gatsby-browser.js so I can use them to identify their scroll positions?
If there's some other better way to achieve the same result I'm open to that of course.
I think that you are over complexing the issue. You don't need the gatsby-browser.js to achieve it.
First of all, because you are accessing directly to the DOM objects (using document.getElementById) and you are creating precisely a virtual DOM with React to avoid pointing the real DOM. Attacking directly the real DOM (like jQuery does) has a huge performance impact in your applications and may cause some issues since in the SSR (Server-Side Rendering) the element may not be created yet.
You are hardcoding a logic part (the ids) on a file that is not intended to do so.
I think you can achieve exactly the same result using a simple function using a few hooks.
You can get the same information as document.getElementById using useRef hook and scrolling to that position once needed.
const YourComponent= (props) => {
const sectionOne = useRef(null);
const sectionTwo = useRef(null);
useEffect(()=>{
if(typeof window !== `undefined`){
console.log("sectionOne data ",sectionOne.current)
console.log("sectionTwo data ",sectionTwo.current)
if(sectionOne) window.scrollTo( 0, 1000 ); // insert logic and coordinates
}
}, [])
return (
<>
<section ref={sectionOne}>Section 1</section>
<section ref={sectionTwo}>Section 2</section>
</>
);
}
You can isolate that function into a separate file in order to receive some parameters and return some others to achieve what you want. Basically, the snippet above creates a reference for each section and, once the DOM tree is loaded (useEffect with empty deps, []) do some stuff based on your logic.
Your document.getElementById is replaced for sectionOne.current (note the .current), initially set as null to avoid unmounting or cache issues when re-hidration occurs.

How To Cache Images in React?

Suppose I have a list of url's like so :
[ '/images/1', '/images/2', ... ]
And I want to prefetch n of those so that transitioning between images is faster. What I am doing now in componentWillMount is the following:
componentWillMount() {
const { props } = this;
const { prefetchLimit = 1, document = dummyDocument, imgNodes } = props;
const { images } = document;
const toPrefecth = take(prefetchLimit, images);
const merged = zip(toPrefecth, imgNodes);
merged.forEach(([url, node]) => {
node.src = url;
});
}
with imgNodes being defined like so:
imgNodes: times(_ => new window.Image(), props.prefetchLimit),
and times, zip, and take coming from ramda.
Now when I use those urls inside of react like so:
<img src={url} />
it hits the browser cache according to the Etag and Expire tags regardless of where the url is used. I also plan on using this to prefetch the next n images whenever we hit n - 1 inside of the view, reusing imgNodes in the same manner.
My question are:
Is this even a valid idea give 100+ components that will use this idea but only 1 will be visible at a time?
Will I run into memory issues by doing this? I am assuming that imgNodes will be garbage collected when the component is unmounted.
We are using redux so I could save these images in the store but that seems like I am handling the caching instead of leveraging the browser's natural cache.
How bad of an idea is this?
You don't need to do it in all of your components. As soon as an image is downloaded it gets cached by the browser and will be accessible in all components, so you can do this only once somewhere in a high-level component.
I don't know what exactly UX you are trying to create by caching images, however, your code only initiates downloading images but doesn't know whether an image is being downloaded, has been downloaded successfully or even failed. So, for example, you want to show a button to change images or add a class to a component only when the images have been downloaded (to make it smooth), your current code may let you down.
You may want to resolve this with Promises.
// create an utility function somewhere
const checkImage = path =>
new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(path)
img.onerror = () => reject()
img.src = path
})
...
// then in your component
class YourComponent extends Component {
this.state = { imagesLoaded: false }
componentDidMount = () =>
Promise.all(
R.take(limit, imgUrls).map(checkImage)
).then(() => this.setState(() => ({ imagesLoaded: true })),
() => console.error('could not load images'))
render = () =>
this.state.imagesLoaded
? <BeautifulComponent />
: <Skeleton />
}
Regarding memory consumption — I don't think anything bad will happen. Browsers normally limit the number of parallel xhr requests, so you won't be able to create a gigantic heap usage spike to crash anything, as unused images will garbage collected (yet, preserved in browser cache).
Redux store is a place to store the app state, not the app assets, but anyway you won't be able to store any actual images there.
This is simple and works fine:
//arraySrcs=["myImage1.png","myImage2.jpg", "myImage3.jpg", etc]
{arraySrcs.map((e) => (
<img src={e} style={{ display: "none" }} />
))}

Categories

Resources