RSSAmplifier
`}}> ); } By default, dangerouslySetInnerHTML will not allow those scripts to execute because it relies on JavaScript's innerHTML , which prohibits it .…"},{"@type":"BlogPosting","headline":"Streaming Text Like an LLM with TypeIt (and React)","url":"https://macarthur.me/posts/streaming-text-with-typeit","datePublished":"2024-10-25T01:07:57.000Z","abstract":"Sam Selikoff shared a slick demonstration of a React hook for animating text streamed from an LLM recently. It caught my eye for a couple reasons. First, it looks great. Second, one of my eternal pet projects is TypeIt , used to create very similar sorts of animations. I couldn't help but create an example of my own using TypeIt, and it turned out to be pretty straightforward – so straightforward…"},{"@type":"BlogPosting","headline":"I didn't know you could use sibling parameters as default values in functions.","url":"https://macarthur.me/posts/sibling-parameters","datePublished":"2024-10-12T15:42:12.000Z","abstract":"JavaScript has supported default parameter values since ES2015. You know this. I know this. What I didn't know was that you can use previous sibling parameters as the default values themselves. (Or maybe \"adjacent positional parameters\"? Not sure what to call these.) function myFunc(arg1, arg2 = arg1) { console.log(arg1, arg2); } myFunc(\"arg1!\"); // \"arg1!\" \"arg1!\" MDN even calls it out (I didn't…"},{"@type":"BlogPosting","headline":"Empty Building","url":"https://macarthur.me/posts/build","datePublished":"2024-10-02T16:46:03.000Z","abstract":"I’ve started to notice my stomach bunch up when I come across #BuildInPublic hashtags, or see people throw around phrases like: “Forget everything else. Just get out there and build.” “It’s all about building cool stuff together.” “The future belongs to the builders.” “Stop talking. Start building.” “So much to build, so little time.” At first, I wasn’t totally sure why I grimace at this stuff. I…"},{"@type":"BlogPosting","headline":"JamComments Now Offers AI-Powered Moderation","url":"https://macarthur.me/posts/ai-moderation","datePublished":"2024-08-25T03:30:36.000Z","abstract":"While AI has absolutely flooded the digital product space for the past ~year, I've been relatively hesitant about its role within digital products. LLMs in particular are incredibly useful (I use them almost every day), but there've been a lot of relatively uninspiring product applications and so much hype. Frankly, many of the product using AI as their flagship feature solve a pain I have to be…"},{"@type":"BlogPosting","headline":"It Might Be Worth Converting that GIF to an Animated WebP","url":"https://macarthur.me/posts/gif-to-webp","datePublished":"2024-08-22T23:26:24.000Z","abstract":"Find one of your favorite GIFs on Giphy and download it. You might be surprised that the result saved to your device won't be a GIF . It'll be an animated WebP. It’s a very intentional move by Giphy, citing the maximization of quality and reduction in load times . After all, we're in a time when page performance has prominent focus in the industry, especially after Google unveiled its Core Web…"}]}

Blog

Alex MacArthur's Blog

I'm Alex MacArthur, a software engineer bossing around computers in made-up languages.

macarthur.meRSS feed ↗75 posts

Latest posts

Your options for preloading images with JavaScript

I just learned that the task of preloading images with JavaScript is surprisingly quirky. There are actually several ways to do it, and the best one to choose can very much depend on the circumstances. Let's explore them through the lens of what I was trying to accomplish before running into all of this. The Scene The latest version of JamComments supports dragging and pasting images into the…

I used a generator to build a replenishable queue.

Ever since writing about them , the generator in JavaScript has become my favorite hammer. I'll wield it nearly any chance I can get it. Usually, that looks like rolling through a finite batch of items over time. For example, doing something with a bunch of leap years: function* generateYears(start = 1900) { const currentYear = new Date().getFullYear(); for (let year = start + 1; year <=…

DNS Resolution Adds Up

I like the satisfaction of finding quick, little wins to maximize the front-end performance of a website. Lately, they've been found by digging through the modern browser's many resource hints . One in particular has caught my fancy: DNS prefetching. It hasn't enjoyed the same spotlight as hints like preload in recent years, but it has a compelling advantage if the conditions are right – namely,…

`document.currentScript` is more useful than I thought.

Every so often, I stumble across a well-established JavaScript API in the browser that I probably should've known about years ago. Examples include the window.screen property and the CSS.supports() method . To my relief, I've realized I'm not always alone in my ignorance. I remember posting about window.screen and getting a surprising amount of feedback from people who also didn't know of it. That…

I think the ergonomics of generators is growing on me.

I like the "syntactic sugar" JavaScript's seen over the past decade (arrow functions, template literals, destructuring assignment, etc.). I think it's because most of these features solved real pain points for me (some of which I didn't even know I had). The benefits were clear and there was plenty of opportunity to wield them. But there are some oddballs in there... like generator functions.…

I guess some request headers are more trustworthy than others.

I got to dabble in some content negotiation recently. I wanted to build an "inspection" page for any image URL served by PicPerf , which would show how effectively the image was optimized. When requested by way of an <img> tag, I'd serve the image, like normal. But when that same URL was directly accessed in the browser's address bar, I’d render some pretty HTML. Here's an example of what I landed…

There are a lot of ways to break up long tasks in JavaScript.

It's not hard to bork your site's user experience by letting a long, expensive task hog the main thread. No matter how complex an application becomes, the event loop can still do only one thing at a time. If any of your code is squatting on it, everything else is on standby, and it usually doesn't take long for your users to notice. Here's a contrived example: we have a button for incrementing a…

We'll soon be able to slide open a `height: auto` box with native CSS.

A couple weeks ago, I wrote about using JavaScript and forced reflows to slide open a box with an unknown amount of content (i.e. "height: auto"). It’s satisfying to understand how to pull it off and why it works. But at the same time, we know in our bones that the approach is more cumbersome than it needs to be. It's a capability that ought to exist natively in CSS, alongside the rest of the…

I didn't know you could compose template literal types in TypeScript.

When template literal types were announced in TypeScript v4.1 , nearly every example you saw used it as simpler way to enforce a choice from a fixed set of options. Prior, it was more common to see this: enum UserRole { Admin = 'admin', Editor = 'editor', Viewer = 'viewer', } function checkPermission(role: UserRole): void { console.log(`Checking permissions for ${role}`); }…

Short-Lived, Tick-Bound Memoization in JavaScript

I'm always looking for excuses to use queueMicrotask() , a relatively sparsely used function for inserting tasks at the end of the current call stack. I might've stumbled across one dealing with short-lived memoization. Humor me for a bit. You've likely seen some variation of this memoization function in JavaScript. Pass in an expensive function, and you get back a different function that'll cache…

Using Forced Reflows and the Event Loop to Slide Open a Box

If you're reading this, there's a more-than-zero chance you've used a CSS transition on max-height to slide open a box. You reached for max-height instead of height because the former will work when the box is sitting at its natural, unspecified height. The latter will not. As long as your max-height is greater than the actual height of the box, you're fine. In many cases, there’s no issue with…

You Might As Well Use a Content Security Policy

A few weeks ago, someone emailed to let me know that JamComments wasn’t playing nicely with his Content Security Policy (CSP). This was the first time I’d heard of the problem, which probably indicates how infrequently the feature is used, despite having been standardized since 2013 and being extremely well-supported by modern browsers. At the time, JamComments used two things obstructed by even…

Avoiding a "Host Permission" Review Delay When Publishing a Chrome Extension

I just wrapped up a Chrome Extension that allows you to convert and download any AVIF or WebP image as a more useful JPEG, PNG, or GIF (it aims to solve one of the greatest pains on the internet ). The extension's very simple, but I ran into an interesting slowdown getting it finished up and submitted for review. Under the "Permission Justification" section of the submission form, the following…

Collect All Requested Images on a Website Using Puppeteer

When I was building PicPerf's page analyzer , I needed to figure out how to identify every image loaded on a particular page. It sounded like a simple task – scrape the HTML for <img> tags, pull off the src attributes, and profit. I'm using Puppeteer, so I started stubbing out something like this: const browser = await puppeteer.launch(launchArgs); const page = await browser.newPage(); await…

TIL: inline event handlers still fire when passed to React's dangerouslySetInnerHTML

Last year, I wrote a post about how to execute <script> tags with React's dangerouslySetInnerHTML prop. Like this: const App = () => { return ( <div dangerouslySetInnerHTML={{ __html: ` <script>console.log("taxation is theft");</script> `}}></div> ); } By default, dangerouslySetInnerHTML will not allow those scripts to execute because it relies on JavaScript's innerHTML , which prohibits it .…

Streaming Text Like an LLM with TypeIt (and React)

Sam Selikoff shared a slick demonstration of a React hook for animating text streamed from an LLM recently. It caught my eye for a couple reasons. First, it looks great. Second, one of my eternal pet projects is TypeIt , used to create very similar sorts of animations. I couldn't help but create an example of my own using TypeIt, and it turned out to be pretty straightforward – so straightforward…

I didn't know you could use sibling parameters as default values in functions.

JavaScript has supported default parameter values since ES2015. You know this. I know this. What I didn't know was that you can use previous sibling parameters as the default values themselves. (Or maybe "adjacent positional parameters"? Not sure what to call these.) function myFunc(arg1, arg2 = arg1) { console.log(arg1, arg2); } myFunc("arg1!"); // "arg1!" "arg1!" MDN even calls it out (I didn't…

Empty Building

I’ve started to notice my stomach bunch up when I come across #BuildInPublic hashtags, or see people throw around phrases like: “Forget everything else. Just get out there and build.” “It’s all about building cool stuff together.” “The future belongs to the builders.” “Stop talking. Start building.” “So much to build, so little time.” At first, I wasn’t totally sure why I grimace at this stuff. I…

JamComments Now Offers AI-Powered Moderation

While AI has absolutely flooded the digital product space for the past ~year, I've been relatively hesitant about its role within digital products. LLMs in particular are incredibly useful (I use them almost every day), but there've been a lot of relatively uninspiring product applications and so much hype. Frankly, many of the product using AI as their flagship feature solve a pain I have to be…

It Might Be Worth Converting that GIF to an Animated WebP

Find one of your favorite GIFs on Giphy and download it. You might be surprised that the result saved to your device won't be a GIF . It'll be an animated WebP. It’s a very intentional move by Giphy, citing the maximization of quality and reduction in load times . After all, we're in a time when page performance has prominent focus in the industry, especially after Google unveiled its Core Web…

Exploring the Possibilities of Native JavaScript Decorators

Exploring the Possibilities of Native JavaScript Decorators – Frontend Masters Boost Native support for decorators is inevitable! It simplifies augmenting class methods, which can help with things like logging, memoization, debouncing, and dependency injection. Alex MacArthur

How to Back Up Self-Hosted Plausible Analytics Data to an R2 or S3 Bucket

Self-hosting the Plausible Analytics Community Edition is appealing for number of reasons, but it isn’t without drawbacks. Among the most obvious: no one’s gonna save you if your data is lost. And the impact of that risk only increases as the amount of data grows. Fortunately, setting up an automated process for backing up that data to a remote location (like an S3 or R2 bucket) isn’t complex or…

On Building Structured Data with Client-Side JavaScript

Web crawlers and client-side rendered applications (SPAs) have had a weird relationship for a while now. Google has long said they’re able to crawl content built with JavaScript, but it’s still been generally recommended that you server-render that content for maximum SEO benefit. That’s always made sense. It’s more laborious for bots to crawl JS-rendered content, and as such, it can take longer…

Control JavaScript Promises from Anywhere Using Promise.withResolvers()

Control JavaScript Promises from Anywhere Using Promise.withResolvers() – Frontend Masters Boost This method enhances flexibility by allowing promises to be resolved or rejected remotely, simplifying and streamlining asynchronous code. Alex MacArthur

Re-Enabling Emails for Ghost CMS Members

This site's content lives in a headless instance of Ghost CMS hosted on Fly.io . I've been very happy with it for a number of reasons , two of which are its writing experience and built-in newsletter support. But I hit an interesting issue after upgrading to a newer version of Ghost (from 5.36 to 5.82.2... it had been a while): I could suddenly only send emails to ~30% of my email list. After…

Adding Structured Data in Astro's Starlight Documentation Framework

I remember when the Astro team first announced Starlight , their documentation framework. The timing was perfect. I had been meaning to overhaul the docs for JamComments and TypeIt , but didn’t want to do so on the shoddy setups they were using at the time. Since then, all of my side project documentation is built with Starlight, and I'm not moving away anytime soon. I‘d still call the project…

It’s Probably Only Getting More Important to Use (Good) Structured Data

Search engines have long been scarily good at understanding content on the web, but there's still a lot to be gained by making explicitly clear what you're trying to say and whom it's intended to help. Tools for doing that have existed for decades – meta tags, sitemaps, and even your robots.txt file. One of the more complex and less understood of these tools is structured data – the specially…

Helpful Guidelines for Posting on LinkedIn

I’ve been observing some of the posting trends on here and it’s clear we could benefit from a few guidelines. You might think I’m unqualified to give them. But the facts suggest otherwise: I won $25 in cold, hard, American cash by winning second place in a community essay contest when I was 12. That’s before most public schoolers learn how to read these days. I have accumulated 10s of followers on…

Looking Forward to (Hopefully) Not Needing Responsive Images One Day

I’ve decided I don’t like the responsive image API in HTML, or at least the idea that we still need it as a performance tool. By "responsive," I mean "show different versions of an image based on device," using only HTML. If it’s been a minute, here’s a recap. You can render responsive images in two flavors: the <img> element with a srcset attribute, or the <picture> element. Let’s say we’re…

You Might Consider Using an Image Sitemap

Despite not being strictly necessary for every site, sitemaps are still an important piece of a solid SEO game, allowing search engines to crawl & index your website’s content more quickly and thoroughly (especially if your site is large, new, or poorly linked). But they’re not just useful for text content. Image sitemaps serve a similar purpose, specifically geared toward images (obvi). You've…

The Architecture Might Not Be the Problem

It’s interesting how language around service architecture changes as patterns fall in & out of favor. People who advocated for microservices years ago now refer to them as “nano” services. It seems to get at the costly hassle it can be to integrate with, iterate on, and maintain a service with such a narrow purpose. Something “nano” is annoyingly small, whereas something “micro” is just the…

Transform Image URLs with a Simple Cloudflare Worker

I've been working on PicPerf in some capacity for about a year now, and I'm still really happy with its API for optimizing, reformatting, and caching images: prefix the URLs with https://picperf.io . But on a number of platforms, that's not so simple. Images are often handled by a proprietary system, with no means of changing those URLs. That's unfortunate because many of those platforms don't…

Hold a Healthy Sense of Caution Whenever Running a curl|bash Command

If you've installed nerd software on your machine before, you've almost certainly executed a command like this before: curl -sL https://some-domain.com/install.sh | bash If you're anything like me, it took a long time (maybe years) to even ask what that sort of command is even doing. It's not much: curl retrieves a response and immediately feeds it to bash to execute on your machine. It's simple,…

It's Never Been Easier to Performantly Put Images on the Web

It probably wasn't in your local news, but something non-trivial in web development occurred early this year: Microsoft Edge added support for the AVIF image format . It was the last of the major browsers to do so, after taking a frustratingly long while, apparently due to licensing issues . Nevertheless, I'm so here for it. I've been waiting a long time to pull the trigger on supporting AVIF in…

Why Am I Getting a 502 Bad Gateway After Turning on Plausible?

If you’re using Docker to self-host Plausible Analytics on DigitalOcean or any other virtual machine, you might’ve run into a “502 Bad Gateway” error when attempting to navigate to the administrator dashboard. It’s a more common problem than you might think, but there are fortunately a few says to figure it out: 1. Give it a minute or two after start-up. If you set up Plausible using the Plausible…

How to Back Up Your Self-Hosted Plausible Analytics Data

Self-hosting Plausible Analytics offers a number of advantages, particularly if you’re keen on owning your data as much as you reasonably can, independent of the constraints any sort of managed plan might offer. But there are trade-offs — the big one being that you don’t get dedicated backups. Fortunately, it’s straightforward to create & restore these backups yourself. Since all of Plausible’s…

Picking the Right Tool for Maneuvering JavaScript's Event Loop

Much of the time, you can get along just fine without thinking a ton about JavaScript's event loop. But sooner or later (especially as you begin spending more time with things like the rendering process and asynchronous tasks) it becomes handy to know not only how the thing works, but the different tools available to best maneuver it. By "maneuver," I mean "schedule code to execute at a part of an…

Raise the API Rate Limit for a Self-Hosted Instance of Plausible

I've been using the self-hosted version of Plausible Analytics for a couple of years now, and I've really enjoyed it . One of the many perks of the self-hosted model is that I'm able to do what I want with my data as often as I want to do it. For example: this is statically generated site. Every time it builds, I pull in the latest analytics data from the Plausible REST API to render on various…

Let's Bring Back JavaScript's `with()` Statement

It's hard not to appreciate the elegance of Kotlin's scope functions , which allow you to tap into object and immediately execute a block of code against it. I often reach for also , run , and let , but with is up there too. Pass an object, and you can access specific properties with no identifier: data class Person( val firstName: String, val lastName: String, val wasRight: Boolean ) val…

Executing Dangerously Injected Scripts Inside React Components

First off, I hereby declare myself not responsible for you shooting yourself in the foot after reading this. I'm doing something a little weird with JamComments integrations. When a site's page is built, all of the HTML, CSS and JavaScript needed for comments to function are pulled in via REST API. I like this pattern. It makes for fewer dependencies, quicker iteration on the product, and it just…

"Server-rendering your UI is expensive!"

Whenever people are duking it out over where UI should be rendered (server vs. client), compute cost inevitably comes up. “Rendering UI on the server is costly and wasteful,” client-side rendering advocates say. I’ve heard that claim a lot, but I’ve not seen much to make me believe it matters. Is server rendering really so jarringly expensive that it’s pummeling your bottom line, or is it a…

You should still look at your site with JavaScript disabled.

Even today, it’s still worth looking at your sites with JavaScript disabled. Many of them have either a couple of elements that depend on JS to show/hire correctly, or a big chunk of the page rendered as a single-page application. In either case, the user interface isn’t “ready” after page load until JS has a chance to download, parse, and execute, which means core web vitals like largest…

Strive for Being "Feature-Complete"

If you own a software package or library, resist the pressure to eternally “improve” or “make it more flexible” it by adding features. Despite what they say, choosing to limit an an API is not the same as abandoning or ceasing to maintain it. It’s a worthy goal for a project to reach the state of being feature-complete, and I’d like to see more maintainers willing to make that decision. You’ve…

There Are a Lot of Ways to Hide Stuff in the Browser

I stumbled across a pull request in Marc Grabanski's modern-todomvc-vanillajs from some time ago, where I was first introduced to the hidden DOM attribute. It's a simple way to hide something on a page that shouldn't be seen in any presentation, It's been around forever, and I had no idea it existed. I took a quick mental inventory before thinking: "wow, there are a lot of ways to hide stuff in…

Lighthouse is Not Your User

You might’ve used Google’s Lighthouse (also built into its PageSpeed Insights tool) for auditing page performance. It’s great! Even fun — the “get a perfect Lighthouse score” game is a personal favorite. Still, it’s good to remember how it relates to the Core Web Vitals. Lighthouse is a _diagnostic_ tool that help you identify opportunities to potentially improve user experience. The Core Web…

Deserializing Polymorphic Lists with Kotlin, Jackson, and Spring Boot

One of my favorite features of TypeScript is discriminated unions, in which a common literal type is used to determine the narrowest possible type of an object. Think of two types of messages – Email and SMS , with types defined as such: interface Email { kind: 'email'; // <- literal type subject: string; from: string; to: string; body: string; } interface SMS { kind: 'sms'; // <- literal type…

For Maximum Accessibility, Be Careful About Using a .dev Domain

It's been an interesting couple of weeks debugging a DNS/connectivity issue for PicPerf 's [now former] domain, so I'm taking the time to write a few thoughts down before it all goes stale. Hopefully, they're helpful to others who run into issues one day. Disclaimer: I’m far from an expert DNS, networking, and firewalls. If you see any incorrect assumptions, here, or have any helpful insight on…

Don't Let Visitors Know Your Origin Server Exists

In its heyday, I was entranced by the Jamstack – particularly by its promise of performance . I knew old-school HTTP caching existed, but I relegated it to being an unnecessarily hard thing boomers cared about. I had moved beyond, believing that a statically generated site is inherently more performant than a dynamic one. No exceptions. That naiveté waned as smart people started sharing more about…

PicPerf's Impact on Jane Ross Tutoring's Website

For decades, Jane Ross Tutoring has been serving students by providing tutoring for a wide range of subjects and tests, particularly in the college admissions space. The website's home page includes a number of headshots from students who were helped by their efforts, some iconography, and a large hero image at the top of the page. This meant that approximately 1,600kb in image weight was…

TIL: A Link’s Download Attribute Won’t “Just Work” for Cross-Origin Resources

I've been working on a small application allowing users to upload media to a Cloudflare R2 bucket. The high-level stack is pretty simple. It's got a client-side piece (React), a middle-tier service (Fastify on Node), with Cloudflare backing the uploads. To no surprise, downloading that content is a part of the work too. When I began thinking through this, I was planning on streaming the objects…