Skip to content

yoga-layout as a component #1

Open
@privatenumber

Description

@privatenumber

yoga-layout offers a lot of features out of the box, but I'm curious if it's possible to move yoga-layout out of the core runtime and abstract it into a tree-shakable component (eg. Box/FlexBox) or a 2nd party component (separate package)?

For context, the main reasons why I want to move away from Ink are:

  • React hooks and data immutability is not ergonomic/intuitive to me
  • File size (largely due to yoga-layout)
  • Yoga-layout can be slow at rendering

Large filesize

yoga-layout-prebuilt alone is around 89.3 kB gzipped (379 kB minified). For reference, Vue 3 is 128 kB minified.

Currently, that means any CLI script that uses this tool will be at least 379 kB minified. This might deter users that are looking to create something simple.

I have two tools that rely on Ink right now, and they're both bloated because of this:

They don't actually do that much. And I don't think they even use the flexbox features. So ideally, I can get them down to less than 50 kB max and I was hoping moving to Vue for SFC compilation + treeshaking could help with that.

Use-cases

Let me know if I'm oversimplifying what yoga-layout does, but AFAIK it implements flexbox in any environment (eg. terminal). This sounds like a great feature, but I'm wondering how often this will be used.

Personally, I don't use any CLI tools that draw boxes, are full screen, need to align content with flexbox powers, etc. Most of the ones I use or make usually have simple and inline UI.

However, it is a great building block to offer. Especially as I read Ink's documentation, it seems like a big selling point to have as a core component. I'm curious if it we can still have it as a tree-shakable component? If the installation size were to deter users, possibly as a separate package?

Slow rendering speed

As I investigated this, I realized there are a few factors that impact rendering speed, and may not even be caused by yoga-layout.

I'll include it anyway since I still found it relevant and insightful for this project.

Rendering performance factors:

  1. The Terminal Emulator used. Native Terminal app is fastest, iTerm 2 is slow, and Hyper is even slower (although, less glitchy re-renders).

  2. Page size. Seems like the larger the render payload, the slower it is. This sounds expected, but it's dramatically slower, which I found unexpected. For example, if my Ink app renders 3 pages worth of data, each re-render is significantly slower than 1 page of data. In iTerm 2, it's glitches as well.

    As a side, I wondered about the possibility of a Page/Document component that overflows content and implements custom scroll so re-renders can be more efficient and less glitchy.

  3. Content change speed. I noticed my Ink app gets notably slower when using ink-spinner. Probably because it changes the render output on every tick, so the render cycle is constantly running.

I originally noticed slowness in my own scripts that use Tasuku. They were running dramatically slower when it was rendering a lot. (ie it was a lot faster to run the script when removing Tasuku). I eventually got an user reporting it: privatenumber/tasuku#5 (comment)

I created isolated experiments with multiple components running 1 second timeouts in parallel to check if it the script will exit in 1s.

Experiment

Code for Ink:

import React, { useState, useEffect } from 'react';
import {render, Text} from 'ink';

const Counter = () => {
	const [counter, setCounter] = useState(0);

	useEffect(() => {
		const timer = setTimeout(() => {
			setCounter(previousCounter => previousCounter + 1);
		}, 1000);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return <Text color="green">{counter}</Text>;
};

render(
	React.createElement(
		React.Fragment,
		null,
		...Array.from(
			{ length: 300 },
			() => React.createElement(Counter),	
		),
	),
);

Result:

Terminal: 300 parallel => 4s

Note, because this is 300 1s timers in parallel, exit time should still happen in ~1s. This might be the real cost of yoga-layout. I would like to remove the factor of page size here by rendering them all in one line, but Ink seems to require all text to be wrapped in <Text>, which separates them into new lines.

npx esno src/ink.tsx  2.53s user 0.27s system 80% cpu 3.502 total

iTerm 2: 300 parallel => 7s

npx esno src/ink.tsx  5.55s user 0.39s system 86% cpu 6.858 total

Adding ink-spinner in the mix seems to slow down the script dramatically:

Terminal: 300 parallel => 36s

npx esno src/ink.tsx  34.79s user 0.94s system 99% cpu 35.809 total

iTerm 300 parallel => 46s (and major CPU lag)

npx esno src/ink.tsx  42.48s user 1.24s system 94% cpu 46.394 total
tasuku
import task from 'tasuku';

const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));

task.group(
	(task) => Array.from(
		{ length: 100 },
		() => task(
			'sleep 1s',
			() => sleep(1000),
		),
	),
	{
		concurrency: Infinity,
	},
);

Result: 100 parallel -> ~6s

npx esno src/index.ts  5.71s user 0.38s system 107% cpu 5.656 total

Result: 300 parallel -> ~ 2m 🤯

npx esno src/index.ts  86.37s user 2.81s system 77% cpu 1:54.47 total

As a separate optimization feature, I wonder how much faster only re-rendering changed lines would be. Alternatively, skipping line clearing and just overwriting instead (and only clearing the extra text at the end, if the new render is shorter than the previous).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions