-
Notifications
You must be signed in to change notification settings - Fork 248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2x faster with SatoriOptions.fonts as a global variable #590
Comments
TL;DR: Declaring SatoriOptions.fonts as a global variable doubled the performance Upon closer inspection of the code, I discovered a global variable named let font: FontLoader
if (fontCache.has(options.fonts)) {
font = fontCache.get(options.fonts)
} else {
fontCache.set(options.fonts, (font = new FontLoader(options.fonts)))
} It turned out, in satori.ts, the cache key was being assigned to the memory address of the We had been declaring SatoriOptions.fonts as a local variable, just as the example in README.md suggests. This made me realize that changing the address of options.fonts with every invocation prevented us from ever utilizing the cache effectively, as it just kept accumulating. Therefore, I declared SatoriOptions.fonts as a global variable as follows: AS-ISexport default async function handler(
request: NextApiRequest,
response: NextApiResponse
) {
try {
const svg = await satori(JSX,
{
width: 1200,
height: 630,
embedFont: true,
fonts: [
// ...
]
); TO-BEconst options: SatoriOptions = {
width: 1200,
height: 630,
embedFont: true,
fonts: [
// ...
],
};
export default async function handler(
request: NextApiRequest,
response: NextApiResponse
) {
try {
const svg = await satori(JSX, options);
// ... As a result, the same code was able to handle approximately double the number of requests. wrk -t10 -c 30 -d30s http://localhost:3000/og/review/41854\?updatedAt\=2024-01-29T14:30:22
Running 30s test @ http://localhost:3000/og/review/41854?updatedAt=2024-01-29T14:30:22
10 threads and 30 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 992.55ms 273.04ms 1.87s 66.67%
Req/Sec 4.43 3.97 20.00 70.93%
891 requests in 30.07s, 318.15MB read
Requests/sec: 29.63
Transfer/sec: 10.58MB
Performance improvement by roughly 1.86 times As you can see, this changes no longer make unnecessary calls to addFonts() in the profiling graph. Now, we can see that the sendData() function, used when returning a response, occupies most of the flame graph. Required
I hope this helps those experiencing performance issues. Thank you. Regarding this, I'd like to inform @shuding , the author of this code, about the matter. If it's not too much trouble, may I go ahead and make the improvements, then submit them as a Pull Request? @shuding |
Wow what an amazing investigation, thank you so much for your effort in this @jakeinflab!
This definitely makes sense to me. Let me know if you have time to open a PR for that, I can also do it if you're busy! |
I'm trying to write a patch for this problem, but I'm finding it difficult to implement, the basic type isn't allowed to be the Key of a WeakMap, and the Object or Symbol is referenced differently each time it's created. |
Frankly, I think it's hard to be fixed within the library, and I think creating a |
Feature Request
Description
Hello everyone,
I am currently running a next.js standalone application in a production environment. Thanks to your hard work, we have been able to generate stunning OG images, greatly encouraging users to share more on social media. I am truly grateful for this.
However, serving the standalone application has introduced some constraints related to the edge runtime. As a result, we have been utilizing satori and sharp in the node.js runtime instead of relying on edge runtime and next/og.
The performance issue has been a concern, though. Combining several React components and 2-3 fonts has resulted in more delay than anticipated. Executing the following wrk command on a MacBook M1 Pro showed a performance of 10-15 RPS. The actual production environment, with less CPU resources, showed about 5 RPS:
Adding the
NODE_OPTIONS=--inspect
option for profiling revealed that theaddFonts()
function, called during the satori constructor execution, was being repeatedly executed. (We have conducted profiling using Chrome DevTools as shown below.)[1] Bottom-Up without grouping
[2] Chrome DevTools - Performance tap
Tracing results suggest that the bottleneck mostly occurs when calling the
addFonts()
function from thesatori()
constructor. While the actual image generation takes only about 20-50ms, 25ms is spent onaddFonts()
. Despite having the font buffer declared as a global variable, unnecessary operations are executed during each satori constructor call due to the processing of opentype fonts. It appears there's no interface for injecting the results ofaddFonts()
, thus leading to the repeated execution of this function.addFonts()
leads to event loop delaysReferencing
src/fonts.ts #L135
, as shown below:It seems each execution of satori() assigns the result of opentype.parse() to an internal variable. I'm curious if there's a way to declare the this.fonts variable globally and either inject it from outside or reuse it in some manner.
Since we consistently use 2-3 specific fonts, minimizing the number of
addFonts()
calls is expected to improve throughput.This is our
/pages/api/og/[id].tsx
.I apologize for my bad English and hope my message is clear. Please feel free to point out any imperfections in my ideas or offer any assistance you can. Thank you for taking the time to read this.
Additional Context
Maybe profiles image can be additional context.
The text was updated successfully, but these errors were encountered: