remix-run / remix-website

347 stars 79 forks source link

Use build-time og images #293

Open kettanaito opened 2 months ago

kettanaito commented 2 months ago

Changes

Why?

The plugin has a nice summary of its benefits. Also summarizing here:

Examples

I tried to re-create what you have with Satori but using Tailwind. Sharing some of the generated OG images below.

GitHub doesn't support uploading WEBP. The images below are JPEG, so sorry for poor quality. Please see them for yourself by running npm run build. The images will be in /public/og.

Post with unique background

fog-of-war

Post without background (default background)

react-router-v6

Post with multiple authors

remix-heart-vite

Post with a long title

incremental-path-to-react-19

⚠️ Build implications ⚠️

Since the OG image generation happens on build time now, that obviously affects the build, well, time! Let's see how.

-3s
+31.37s

Although this generates the images for all 27 existing blog posts (<1s per image), that's a huge decrease! I will see what I can do on the plugin's side to make it faster. The plugin supports caching, and will not generate images for data that hasn't changed, but that cache won't survive the build on CI, I wager.

Roadmap

kettanaito commented 2 months ago

Deployment

A single requirement this plugin has is for your build to run a Puppeteer binary. You seem to be using Fly, which I believe is pretty flexible in terms of Docker images you use to deploy your app.

Can someone please double-check if you have Chromium installed already? If not, we should add that to the image to skip downloading it on every build. cc @brookslybrand

If this would be a complete no-go, you can also upload the generated images manually anywhere (e..g a CDN) using the writeImage option. It gives you the generated image's buffer.

kettanaito commented 2 months ago

I think with this linting rule, you may want to enable verbatimModuleSyntax option in TypeScript. It will force type imports on the transpiler level.

Warning:   5:1  warning  Import "OpenGraphImageData" is only used as types  @typescript-eslint/consistent-type-imports
kentcdodds commented 2 months ago

This is neat! The problem with this approach is that it requires a rebuild of the whole site when new posts are published or a change is made to the og images. Additionally, the build times increases linearly with the number of pages which can aggravate the rebuild issues. Taking this to the logical conclusion is why Gatsby and Next.js came up with Deferred Static Generation (DSG) and Incremental Static Regeneration (ISR).

Having things be generated at runtime is a desirable feature for these reasons.

kettanaito commented 2 months ago

Thanks for the feedback, @kentcdodds! Those are valid concerns, allow me to address some and share my opinion on others.

Changes require a rebuild

This will be my most controversial opinion, perhaps, but I truly believe that runtime content rendering, while it has a place to be, is a bad idea for most people. In practice, blogs, portfolios, and personal websites are static by nature. Pushing the content rendering to runtime adds unnecessary complexity and fragility (what if my runtime function pulls from a malformatted MDX? The site is down).

I understand that may be desired in the case of highly-dynamic website, like yours. Realistically, very few people build or even need that level of detail. In fact, when I created a site for my wife using Remix, I quickly learned that it was a mistake to use it for a de-facto static website (it's still one of my painpoints with the framework). It introduces a degree more issues compared to the value it provides (I have to use verbose runtime MDX rendering, Remix doesn't support relative assets like images so I need to create explicit resource routes for those, etc).

I don't mean to criticize your setup by saying this! Sorry if it sounded like it. I just want to be objective. From my experience, it felt like it solved problems I didn't have. I know it's, before anything, a "me" issue. But I also suspect there's quite a lot of "me"s around there building their blogs who will be overwhelmed just as I was.

That being said, the point itself is valid. Having to wait for an entire build to finish just to see an update to an OG image is likely too much. I think the solution here is asset caching during the build. remix-og-image already has caching built-in. It won't generate anything if there's been no changes to the relevant content routes. I need to see how to make its cache persist across builds, and then you will only wait on generating the stuff that actually changed. I believe that would address your concern.

Linear build time increase

I agree on this one too. I mean, it's x10 build time increase as of now, that's not nice!

I have at least four things to try to improve the plugin's performance:

  1. Spawn the browser earlier (right now the plugin waits until the end of the build).
  2. Spawn the Vite preview server earlier (the same story).
  3. Fetch all OG routes' loader data in a single fetch using _routes (right now, the plugin does a request for each route individually). This will only affect Remix apps with the Single fetch enabled (while it's experimental anyway).
  4. Optimize the way it spawns pages in Puppeteer.

I believe implementing these will yield to a decrease in build time.

It's worth mentioning that build-time generation will take time by design. As long as that time is appropriate, I'd argue it's a fantastic price to pay for all the benefits that the build-time generation provides, proper rendering and easier setup being just a few. If you consider how much compute this solves for your app in the long run, it's more than worth it.

Generated images should also be cached as the rest of your build assets. Most major hosting providers support build caching in one form or the other. If the generated images are cached, you pay no build time if nothing has changed, which accumulates to an even bigger time save than caching a generated image on runtime.

DSG and ISR

Both are fantastic techniques that solve real problems. At the same time, I must admit, I've never faced any of those problems building websites and publishing content for almost a decade now. I can safely say most people haven't either.

The majority of folks don't have millions of pages. Not even hundreds. It's good if they have a dozen! Transforming MDX is really fast (that's one of the reasons you can put it on runtime, to begin with). I can pay that price for 10 of my articles once, instead of asking every reader to pay it whenever they open my article for the first time.

There is an important exception. If your content is dynamic, as in something on the OG image changes independently from your post (like GitHub's PR title on the PR OG image preview), then this plugin is not for you. Then, the runtime generation is indeed a solution to a problem here. But it's not in all the other cases, which I believe is the majority (like this website!).


Your feedback comes down to performance and optimization, and I'm happy about that! The plugin is extremely raw, and trying it out on the Remix's site gave me a lot of hopes in its future. The fact that I was able to re-create your setup being entirely unfamiliar with the code base and the previous setup in under an hour is incredible. I spent an hour tweaking Satori just to understand it forces me to load a TTF version of my font that is x3 larger just so I could generate an image. That doesn't sound like a good design to me.

I have to emphasize I don't see this plugin as a panacea. Neither do I see the runtime image generation as one. Those are two different techniques to solve different problems. It's bad when you employ a tool to solve problems you didn't have, to begin with. This applies to both of them. This plugin won't always make sense, and I will make it abundantly clear in its repo what it's for.

So, I will keep working on the plugin's performance and let's see where it goes!

kiliman commented 2 months ago

When I was building out my blog, which ran on Cloudflare Workers, I decided that I wanted my MDX content to be compiled at "build" time. My content was static files committed to the repo, so I had a script using GitHub Actions that compiled the MDX and pushed the results to the Worker KV store. This way, there was no runtime cost. It even included Etags, so I didn't even need to send the result if it was already cached.

For local development, I created a content watcher that would compile and store in local KV whenever I saved the MDX file.

This resulted in a very nice workflow that optimized for reading as well as change.

kettanaito commented 2 months ago
kettanaito commented 2 months ago

I am looking at the Fly Docker image to install Chromium, allowing this plugin to run (the default image doesn't have it installed). Will try to push the changes later next week.

Already have this plugin confirmed working on Cloudflare, expect Vercel to be the same. Not sure why Chromium is not installed as a part of puppeteer on Fly.

kettanaito commented 2 months ago

Updates

With https://github.com/kettanaito/remix-og-image/releases/tag/v0.3.5 and preceding releases, I've shaved about 3s from each generated image. I've also added proper caching on CI, which should be supported in Fly as well. This should improve the build time and produce dramatically faster incremental builds.

I've also added browser.executablePath option since on Fly it looks like we'd have to install Chromium and provide an exec path to it manually. Tweaking the existing Docker config to do that...