I found a bit more ways of iterating through the layers tree in Figma and I wanna share them with you. The first one is the one I already explored. It’s pretty fast, but compared to findAll
it’s still a lot slower. About 2x slower on my current computer.
The first method I showed above already. Unfortunately, as I said, I can’t use it since there is no way to make it async.
function traverseRecursive() {
console.log('Normal recursion') // 10s for 45k nodes
function recursive(n) {
count++;
if (!n.children) {
return;
}
n.children.forEach(recursive);
}
recursive(figma.currentPage);
}
The second method does essentially the same thing, but much slower. This is why I wanted to switch to forEach
but couldn’t and didn’t know why it was faster. Seemingly, we only changed forEach
to for loop, which is necessary if you want to also make this function async.
But why is it slower? The mistake I made was getting the .children
property of the element too many times! It turns out, it’s a pretty slow operation. So reading it a couple of times here and there makes everything very slow.
function traverseRecursiveSlow() {
console.log('Slow recursion') // 18s for 45k nodes
function recursive(n) {
count++;
if (!n.children) {
return;
}
for (var i = 0; i < n.children.length; i++) {
recursive(n.children[i]);
}
}
recursive(figma.currentPage);
}
To fix this rookie mistake, simply save it to a variable. Even when you read its length, you should be reading it from a variable! Note that I’m using children.length
to get length instead of n.children.length
as the latter may double (or quadruple in bad cases) the traversal time!
function traverseRecursiveFast() {
console.log('Fast recursion') // 10s for 45k nodes
function recursive(n) {
count++;
let children = n.children
let length = children.length
if (!children) {
return;
}
for (var i = 0; i < children.length; i++) {
recursive(children[i]);
}
}
recursive(figma.currentPage);
}
Our next contestant is the same function, but reverse. Again, I’m saving node children to a variable here to make the search faster. It’s not reversed in order, simply the logic in the code is flipped. But the idea is the same and you can choose basically what is more readable/convenient for you. This one accepts an array of nodes instead of a single node.
function traverseRecursiveReverse() {
console.log('Reversed recursion (fast)') // 10s for 45k nodes
function recursive(n) {
let len = n.length;
if (len === 0) {
return;
}
for (var i = 0; i < len; i++) {
const node = n[i];
count++;
const nc = node.children;
if (nc) {
recursive(nc);
}
}
}
recursive(figma.currentPage.children);
}
Next, we have another pretty cool method! It is taken from this awesome article: Using ES6 Generators To Recursively Traverse A Nested Data Structure. I actually took inspiration for the previous method from this one. This one is pretty interesting because it basically allows you to traverse a tree in linear order, similar to findAll
, but with a possibility to make it async without even having to make the traversal method async! For my case I put the await statement for the progress bar in the while
loop. It seems to even be slightly faster than the for
methods, however I’m not entirely sure why.
// 9s for 45k nodes
function traverseGenerator() {
console.log('Recursive generator method')
function* processData(nodes) {
const len = nodes.length;
if (len === 0) {
return;
}
for (var i = 0; i < len; i++) {
var node = nodes[i];
yield node;
let children = node.children;
if (children) {
yield* processData(children);
}
}
}
var it = processData(figma.currentPage.children);
var res = it.next();
while (!res.done) {
count++;
res = it.next();
}
}
And finally, the fastest of all but unusable for me as I can’t show the progress bar and let the user to cancel the plugin if it’s taking too long: Figma’s built-in figma.findAll
.
// 5s for 45k nodes
function traverseFindAll() {
console.log('Using figma.findAll')
figma.currentPage.findAll((n) => count++);
}
And here is the code to finish it off, if you want to run this plugin for testing. Don’t forget that you can only use one function per plugin run as the search results of previous functions will be cached, making next traversals faster. Full code is available as a GitLab Snippet too.
let count = 0;
let start = Date.now();
traverseRecursiveSlow(); // change this func. every run
console.log("nodes:", count, "time:", Date.now() - start);
figma.closePlugin();
By the way, I was using the Figma’s Design System UI2 as the test subject: https://figma.fun/7FwJwy and a bit of Ant Design System: https://figma.fun/1m5kOG (helped me figure out that my slow method was extremely slow on long arrays of nodes, which UI2 doesn’t have).