This is a post inspired on the experience I had while investigating a performance problem at my current project, where it helped speed up an expensive action by 92%.
The scenario
Imagine we have two instances of a container component, and one instance of a content component. With a button, we toggle whether the content component is in container A or container B.
const [isInContainerA, setIsInContainerA] = useState(true);
return (
<>
<ContainerA>
{isInContainerA && <Content />}
</ContainerA>
<ContainerB>
{!isInContainerA && <Content />}
</ContainerB>
<button onClick={() => setIsInContainerA(!isInContainerA)}>
Move content
</button>
</>
);
When we click the button, <Content />
is unmounted from A
and mounted again in B
or vice-versa.
The problem
What if <Content />
was a heavy component? If mounting and/or unmounting Content was costly, the interaction would take long and feel laggy.
In our case, we had around 20 expensive Content components and 10 Containers. With a button click, all instances of Content needed to be redistributed among the Containers.
What didn’t work
Lighter Content
It may seem obvious, but this should be the first approach every single time. However, sometimes the Content component is just too complex and it’s not a matter of inefficient code. After we made sure we were reasonably close to optimal on this front, we had no recourse but to look elsewhere.
In our case, the expensive Content element also included a video feed, so the mounting latency was particularly critical. If remounting Content was slow, it would be more noticeable due to the lost video frames.
Absolute positioning
One solution we explored was rendering the Content elements absolutely-positioned and creating the illusion of them being inside the container by carefully setting their coordinates. They would actually be children of a parent that’s independent from the containers, thus avoiding the remount.
In a simple scenario, such as the example, this solution can work. But start adding complexity and the math quickly becomes unwieldy. By the time there are multiple containers and complex positioning algorithms (like a grid), it’s almost impossible to get it right.
All in all, it’s an error-prone approach with scalability and maintainability problems. Steer clear.
What did work
Unsurprisingly, googling did the trick. There’s nothing prebuilt into React to solve this problem, but there’s a nifty library that can take care of it.
Our use case matches not one but two of the scenarios described in their docs:
- Your elements are expensive to render, and you’d like to render them once and then place/unplace them later (e.g. a reusable pool of expensive-to-render elements that can be shared among different parts of your application).
- Your DOM elements have built-in state (e.g. a playing
<video>
element), and you’d like to move the element elsewhere without losing that.
Using the library is simple: you create an InPortal
, render whatever you want inside, store an identifier node, then render wherever with OutPortal
. Just make sure you don’t try to render the same content through two OutPortals at the same time.
import { InPortal, OutPortal } from 'react-reverse-portal';
const [isInContainerA, setIsInContainerA] = useState(true);
const portalNode = useMemo(() => portals.createHtmlPortalNode(), []);
return (
<>
<InPortal node={portalNode}>
<Content />
</InPortal>
<ContainerA>
{isInContainerA && <OutPortal node={portalNode} />}
</ContainerA>
<ContainerB>
{!isInContainerA && <OutPortal node={portalNode} />}
</ContainerB>
<button onClick={() => setIsInContainerA(!isInContainerA)}>
Move content
</button>
</>
);
In our case, we had to save the portalNodes into a React Context (we predictably named it
PortalsContext
) to avoid prop drilling.
It’s not for every case
Flash forward a few months after we implemented this for our video elements. Now we were fighting the slow mount of a rich text editor component, and we reached for React Reverse Portals again.
Since we were facing some deadline pressure to get it to work, we kept a journal in Notion, mostly so every developer could know what avenues had already been explored. Here are some fragments from it.
Andrés, 28 Jul 2022: reverse portals seem to be the ideal solution (…)
Andrés, 29 Jul 2022: managed to make the virtualization work inside the reverse portal by sending the scrolling container through the portal. (…)
Andrés, 8 Aug 2022: we decided to revert the reverse portal because the solution I found to make it work with virtualization messes up the (…)
Javi, 9 Aug 2022: problem solved! after another look into why the mounting was so slow, a quick fix improved mounting times by 200%.
What can we learn from this?
-
First, that overusing Reverse Portals can obscure the actual problems. We had an underlying issue that was fixed with a few of lines of code. We would have never invested time into finding that problem if we had used reverse portals to make it feel snappy from day one. Why does this matter, if it still runs fast? It does, but there’s a very tangible drawback: the initial mount would’ve still been slow.
-
Second, that reverse portals don’t always work as you expect. Since the portaled content can move to a new location without re-rendering and that’s unusual, it can have some unintended effects. In our case a third-party virtualization library was built on the assumption that this would not happen.
-
Third, that tech jargon is crazy and we almost never stop to notice that. If you’re not a developer, the second and third entries from the log look like something out of a science fiction story. Probably right before the experiment goes wrong and becomes a danger to humankind.
Conclusion
Improving client-side performance gets complex and increasingly frustrating as your app grows. Understanding the causes (such as expensive mounts) is key, as is knowing where to look for help. Within the React community there’s almost always a solution to be found instead of reinventing the wheel. However, be aware that sometimes the wrong tool can obscure a problem instead of solving it.
That’s all for today. Now you’re thinking with Portals.