Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
42

Why I left Instagram

from Liz Denys [alt+shift+b] in programming

Content warning: discussion of fatphobia in advertising Practically overnight, my ads on Instagram went from annoying but benign reminders of capitalism to an utterly dehumanizing nightmare. Suddenly, every third or fourth photo of my friends' latest adventures with their kids, pets, and travels near and far was punctuated with liposuction ads. Effective immediately, I've stopped reading and posting to Instagram because I refuse to subject myself to repeated messages pushing a potentially deadly procedure because people fear my "unruly" body. Honestly, I'm disappointed that I've held out for so long here. I've spent a painful amount of time tweaking Instagram's advertising algorithm before by "hiding" ads and blocking accounts so it would stop showing me shapewear and diet apps - something I should never have had to do in the first place. Plus, hatred of fat bodies like mine has always been rampant on Instagram: many highly successful influencers' platforms are rooted in fatphobia, often directly. I've previously delayed leaving Instagram because I knew I would lose social connections to people I care about, and I will grieve the connections I'm sure to lose. At the end of the day, the responsibility to create a safe platform for people no matter what body they inhabit should not be overwhelmingly borne by the people it harms. Footnotes Regrettably, I will not delete my account as I need to maintain my namespace. I've been impersonated on Instagram before and do not want to allow an impersonator to have the username that most people would assume is actually me.↩ I am lucky that my livelihood does not depend on creating content on platforms like Instagram; others do not share this privilege.↩
18th Apr 2022

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Liz Denys

Lilypad mini quilt

While on a short road trip and then recovering from surgery, I hand quilted and hand embroidered a linen lilypad quilt. And yes, I did make this little quilt for my Frinos plush from Hades II to have a nice place to rest.

5th Jul 2026 • 1 votes
Things to consider when choosing a community ceramics studio, partial list

I've worked out of a handful of different community studios at this point, so I figured I'd share a (probably incomplete) list of you might want to consider when picking a community ceramics studio for your practice. Cost, location, value: I'm not going to comment too much on this as costs vary highly depend on what city/metro area/etc. you're in and you best understand what value you'll get and what you can afford. That said, if you live somewhere walkable and are planning on making at home, you might want somewhere you can walk to or get to with fewer transfers or shorter walking to make transporting fragile greenware simpler. Regardless of your budget, it's good to know all the costs up front: class tuition, membership fees, open studio time fees, firing fees, materials costs, etc. Also, if you live somewhere with a high demand for community ceramics studio access, you may not get to be that picky if you want to start sooner! Equipment and space: What equipment is provided? What condition is it in? How many wheels? How many wheels per member? How many wheels per student? Is there a slab roller? What size? Do they have dedicated canvases or mats for different color clay bodies? Which do you use if you use a non-studio claybody (if you're allowed). Is there an extruder? Which extruder dies does the studio have? Can you use your own if you buy more or make custom dies? What are the policies for who gets to use what equipment and training? Stricter policies usually means equipment will be in good condition, but only if it's enforced. Ask about how often equipment has needed repair and how to report issues, even if they have good policies. What do the workspaces look like? How much space is generally available per person? What kinds of chairs and stools are provided? Are stools at wheels adjustable? Are there bricks or similar available for resting feet on if that helps your wheel positioning? Are handbuilding and glaze area seats comfortable for you? What height are those work areas/chairs? This may not seem very important, but standing height tables can be much less comfortable if you're primarily going to sit. What does your storage space look like? Are shelves shared or do you get dedicated space? How much capacity and weight work on the shelves and how does that fit into your practice? Remember you may be storing tools and clay/reclaim as well as works in progress. There can be lots of politics around shelves - ideal ones are neither too high nor too low, all else equal, and can be very competitive to get. Highest shelves may also be highly coveted since you may be able to stack things very high and store more! How easy is it to move around the studio? How easy is it to get your pieces from the storage space to the working areas to the places where you set them to fire? Are there separate spaces for members and students? Even if there are dedicated student classrooms, do additional classes spill over into other areas or shared areas? How frequently? Is this information easy to find out before you get to the studio? Does the studio provide bats and wareboards? Remember, even if bats and wareboards are provided, you may want to bring your own that you can keep in better condition or have different properties. Are there shared studio tools? What condition are they in? Is there communal newspaper? Communal plastic for slowing down drying? Newspaper is easy to get yourself, but dry cleaning bags are hard to buy in individual-use quantities. Materials: What are the studio claybodies? Do they fit well in your practice? Are they smooth or groggy enough? Which colors of claybodies are available? How durable and food safe are they? Will they reach vitrification and achieve low absorption rates if you're making dinnerware or other vessels intended to hold water? Is clay communal or do you purchase your own? If so, does the studio reclaim for you or is it communal practice? What is done to ensure reclaim isn't short? If clay is not provided, can you buy it at the studio? 25 pound blocks of clay are heavy and annoying to transport if you walk, bike, and take transit everywhere like I do, so this can be a big deal! Can you do agateware/neriage/nerikomi? (Make sure you're doing this with compatible clays, of course.) This question is especially relevant if clay is communal as you cannot simply throw blended clay blocks into reclaim bins for a single clay as this would create a new claybody unexpectedly. Maybe you are required to reclaim yourself, maybe there's a miscellaneous reclaim, or maybe they accept a small amount of loss (remember you can minimize this by ensuring you use all your blocks up - the end can usually be wedged up into a new claybody and made into a little pinch cup or bowl worst case). Can you use outside claybodies? If yes, what's the approval process? Even if you are not personally interested in using outside claybodies, it's good to ask as free-for-alls can result in low fire clays melting all over shelves and ruin other people's work at hotter temperatures. Does the studio provide decorating slips and underglaze? If they don't provide, can you bring your own in? What's the approval process? Can you make your own decorating slips or colored claybodies with Mason stains or oxides? What's the approval process? What are the studio glazes? Are they clearly labeled as food safe vs. not? If multiple claybodies are common in the studio, how many glazes fit each and is this clearly labeled? Are there test tiles for every studio glaze on each studio claybody? Are there any test tiles for glaze combinations? Reminder that test tiles aren't a replacement for doing your own testing - different surface slopes, your dips may be longer or shorter - but they help give you a sense of what's compatible with which clay bodies and which are runny vs. stable. How and how often are studio glazes tested? Are new batches tested before they're put out for use? Are studio glazes typically kept in consistent conditions? Large buckets of glaze can change consistency over time. Is specific gravity monitored periodically so water can be added as needed? Are glazes flocculated well/kept well in suspension or are they often hard-panned? Can you use non-studio glazes? Typically, this means commercial glazes as homemade opens a gigantic can of worms. What's the approval process? Reminder that low fire materials used in higher temperature firings can mess up more work than just where they're used, so even if you're not planning on ever using non-studio glazes, it's good to know what checks are in place to avoid this issue. Does the studio provide wax? Wax brushes? What condition is it in? Does the studio provide oxide washes? Are you allowed to make your own if it doesn't? What's the approval process? Firings: What temperatures does the studio fire to? How does this match their claybodies? A lot of claybodies, especially commercial claybodies, claim to support a range of firing temperatures, but they're not fully vitrified at all of those temperatures, especially at the lower end of their ranges. If a studio fires to both cone 6 and cone 10 and you plan on working primarily with cone 6 glazes, either studio or commercial, and make food-safe pieces, is there an appropriate claybody that will reach vitrification or are they secretly really all meant for cone 10? If you're only interested in making non-functional work, this may not matter to you as much as someone making mugs or vases. Are there special firings, e.g. luster firings? If there are luster firings, make sure to inquire about their safety protocols around luster firings - even if you plan on never using luster, poor safety policies around luster can endanger the health of everyone in the studio. How quickly is work turned around? Faster isn't necessarily very important once you've settled in as you want to be in a flow that has work at all stages at all times so you always have something to do, but it can be helpful to plan your rhythm. What is being done to ensure quality materials and firings? Are cones used in every firing? Is work that is too wet or egregiously overglazed set aside or fired alongside everything else - causing frequent explosions and glaze mishaps that affect other people's work? Cleanliness and safety: How clean is the studio? If you can visit on a few different days, that might help. Do people generally clean up after themselves well? Don't forget to check glazing areas and the undersides of tables and such - lots of clay dust can hide there! Are people taught safe practices (wet cleanup, no dry sanding, etc.)? How often do they actually follow them? What supplemental cleaning does the studio staff do? How frequently? Are there air filters? What quality of cleaning do they provide and are they appropriately matched for the size of the studio? Are there air quality monitors? Note: this doesn't seem very common to find at community studios in my area, but it would be a good sign if the studio did! Access and security: Are there signups for specific space to use or is it a free for all? Signups can be frustrating, but you're guaranteed specific space at a specific time. I've noticed people are better at sticking to an appropriate amount of space for their work that's mindful of those around them when it's a space they have to sign up for, but the flexibility of being able to drop in whenever can be great, too! If studio time is signup-based, are new slots released at clear, consistent times that work for you or will you always be getting picks long after most others do? Do students and members have 24/7 access to the space or do they only access to the studio when it's staffed or it's their class or for times they've signed up for? When is the studio open? It can be especially helpful to check in on drying and potentially spray a piece with a little water if it's getting too dry before you're able to do work, even if it's not a time you can do work. It's also great for getting a sense of how quickly pieces dry at the beginning of new seasons/studio temperature and humidity conditions. Who's allowed in the studio? Are there video cameras? Staff on site? Studio flow: Getting a sense of a studio's flow is easiest by seeing and trying out the studio! Take a tour, attend a workshop, take a class! If you're interested in membership, it may still make sense to take a multiple-week class first, and at least in Brooklyn, a lot of studios require you to take a class at their studio before getting a membership to learn how their studio operates day to day. If you're interested in membership, note that membership flows and class flows might be different - different spaces, different open studio policies, different storage situations for work and tools, etc. for example, a studio I loved to work out of as a member was a terrible experience during a handbuilding class because it tried to cramp too many people in too small of a space, but members weren't in the member handbuilding areas as much all at once, so I could finally enjoy the studio. Also, students often share a communal shelf in my area, while members have more dedicated storage space, allowing for more projects at once and storing more tools and other items used in their practice. Community vibe: Is it more social? More get down to business? Loud? Quiet as a mouse? How do you vibe with the community? Does the studio have a code of conduct? Is it a good code of conduct? Does wearing headphones generally mean "don't bother me" or do people interrupt everyone frequently? How do the owners and staff interact with staff? How do owners and staff interact with students and members? What are their expectations of members? I've been at studios where the owners and staff really don't want to ever interact with you, but I also once worked out of a studio where the owner would roll in and interrupt whatever was going on to use whoever was at studio as their personal therapist... even though it was time those members and students were paying her to use the studio. While the latter is a pretty extreme example, I've heard from ceramist friends in many areas that it's sadly not that uncommon to encounter overbearing owners that sour the experience. Miscellaneous: Are you allowed to sell work you make there? Are there community shows/sales? Are there official or unofficial limits on the volume of work? Some studios only want potters who aren't making very small quantities of work or only small to medium sized pieces because they don't actually have the capacity for high throughput or larger work... but not all of them will have clear policies on this. If there's a limit but it's only an unofficial policy, it will likely be applied unevenly. Finally, while it's unlikely that every community studio, or perhaps even any community studio, will be a completely perfect fit for any given potter, I've personally found my practice to be enjoyable at every community studio I've been at with a good community vibe. The goal of this list isn't to find a reason to avoid any particular studio as much as knowing how a studio's choices or limitations might affect your practice so you know what to expect and how to adapt!

10th Mar 2026 • 1 votes
WWII watch cap crown shaping

Like so many other hat knitters, I've knitted the classic WWII watch cap, which is also known as Beanie no. 212, more than a handful of times. (You can find it on Ravelry here and here, respectively.) It's a charming, quick knit that's easy to resize, but I've never really liked the crown shaping on the original. I've knit a lot of these hats, and only one's had the original crown shaping for the decreases. I'm sharing two crown shaping variations I keep going back to below. I always knit this hat in the round, and my crown shaping patterns assume you will be knitting in the round, too. Alternate crown shaping 1 This alternate crown shaping follows the 6x2 ribbing of the previous section for the bulk of each round so that the ribbing continues throughout the entire hat. Written out: Divide stitches on 3 double pointed needles with stitches evenly divided among the three needles or mark out even sections with stitch markers if decreasing across 2 circular needles or with magic loop. (k1, ssk or skp, k if previous row was knit or p if previous row was purl until 3 stitches are left on the needle/before the next marker, k2tog, k) x3. Repeat step 1 until 4 stitches remain on each needle/in each marked section. Cut and draw yarn through the remaining 12 stitches 2-3 times. Tie off and weave in the ends. Alternate crown shaping 2 This alternate shaping both maintains the ribbing throughout the crown shaping and accentuates it by creating a sort of three-pointed star with 3 of the 2-stitch-wide purl parts from the previous 6x2 ribbed section. This shaping really only makes sense when making a size hat where the number of stitches is evenly divisible by 24, so I typically only do it on size "small" hats (cast on 96 stitches). You could adjust the needle size and gauge for a yarn that works well at that gauge to make this work for different sizes. Written out: Stop the last 6x2 ribbed row 1 stitch early and divide stitches on 3 double pointed needles with stitches evenly divided among the three needles or mark out even sections with stitch markers if decreasing across 2 circular needles or with magic loop. Ensure that each needle transition or marker has a single purl stitch on each side. (p1, ssk or skp, k if previous row was knit or p if previous row was purl until 3 stitches are left on the needle/before the next marker, k2tog, p) x3. Repeat step 1 until 4 stitches remain on each needle/in each marked section. Cut and draw yarn through the remaining 12 stitches 2-3 times. Tie off and weave in the ends. Additional project notes Needle: US 7 - 4.5 mm Gauge: 19 stitches and 24 rows = 4 inches in stockinette Hats made for me should generally be a small (cast on 96 stitches), and hats made for my partner Matt should generally be a medium (cast on 104 stitches). Footnotes Either of these will create a left-leaning decrease, use whichever one you prefer.↩ Ibid.↩

15th Dec 2025 • 1 votes
NYC Bike Rules for Drivers mini-zine

After five different drivers, including a school bus driver, aggressively close-passed me in Midwood last night and two of them threatened me just for being on the street, I realized there's a huge gap in educational outreach about bike rules in NYC: drivers. I decided to make a mini-zine that demystifies the behavior of law-abiding bicyclists like myself and helps drivers better understand what to expect when sharing the road: All text and illustrations in the mini-zine are my own. Many thanks to Matt Denys for help proofreading! This zine is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License, so you can copy and distribute this zine for noncommercial purposes in unadapted form as long as you give credit to me. Gas stations, car dealerships, auto repair shops, rental car offices, parking lots, book shops, restaurants, coffee shops, and other commercial establishments are welcome to give out these zines, too - as long as the zine itself is free! Check out the NYC Bike Rules for Drivers mini-zine on the web or download the pdf to print here!

21st Oct 2025 • 1 votes
How to fold a mini-zine

Here's how to fold a mini-zine, featuring my mini-zine So you want to make a public comment! Fold the printed mini-zine page in half, then half in again, and then in half again as shown. When you open the piece of paper, each of the pages should fit fully within a set of folds. With the mini-zine page completely open, fold it in half with short ends touching ("hamburger style") so the text is on the outside. Using scissors, cut a across the dotted line. When you reopen your paper, there will be a slit in the middle of the sheet. Fold the paper in half lengthwise with long ends touching ("hotdog style"), hold the paper at either end, and fold the sheet into itself to form an 8-page booklet. Make sure the cover is on the front!

15th Aug 2025 • 1 votes

More in programming

Must we always use LLMs this way?

As with my last post on AI, let me open with a statement: Just because most people hold AI a certain way, doesn’t mean that we always must, either now or in the future. As I noted in that piece, and my shades of gray post, I am well aware — and agree with — … Continue reading Must we always use LLMs this way? →

4 hours ago • 1 votes
TokyoDev Welcomes Its Head of Operations

TokyoDev has grown substantially over the past few years. We’ve expanded our community, dramatically increased the scope and frequency of our events, and have curated a library of articles for international developers. Despite all that, the way we run the business hasn’t changed much. I’ve continued to coordinate most of these activities myself, making the final decisions and keeping everything connected. In short, TokyoDev cannot run without my daily input. That approach worked well when TokyoDev was smaller, but it’s increasingly limiting how much we can accomplish. To help the business grow further, I wanted to find someone to take over some of my responsibilities and bring a fresh perspective. This is a significant step for me personally, as while I have worked with many contractors over the years, I have never actually hired a regular employee. Most importantly, I wanted to ensure that this person was someone I could trust to understand what TokyoDev stands for, and to make decisions in line with our philosophy. So I’m happy to announce that Scott Rothrock is joining TokyoDev full time as our Head of Operations. About Scott Scott has lived in Japan for nearly 20 years and has worked as a software engineer in multiple industries and environments, including small Japanese-owned businesses, overseas employers, and international companies based in Japan. Until the end of October 2026, he was the head of the Japan Markets team at Kraken Technologies, where he led an international team of software engineers in delivering complicated, regulated products on strict timelines for Japanese electricity and gas retailers, while also ensuring compliance with evolving organizational and global standards. He’s made a name for himself both inside and outside Japan as a kind, effective mentor who can cultivate great engineers and leaders. Scott’s previous role in TokyoDev Some of you may already know Scott in another capacity, as he has been involved with TokyoDev since 2022. He began as a community volunteer and later became our community manager, which also included responsibilities such as managing our article pipeline from submission to publishing. When I created TokyoDev’s Discord, I had no previous experience running an online community. Scott joined soon after I started and began helping other members, stepping in when people behaved inappropriately, and configuring our roles and permissions and safety features. I was impressed by his initiative and asked him to become an official moderator in January 2023. Outside of his role as a moderator, Scott has always pushed TokyoDev to grow; for example, he arranged internet events such as livestreamed AMAs and interviews. He organized TokyoDev’s first meetup as a volunteer in February 2023, which taught me that there was interest in TokyoDev events outside of the internet. By the end of 2023, I had brought him on as our part-time community manager. What I saw throughout his tenure was that Scott continued to take initiative in expanding TokyoDev. Scott was one of our first external contributors, writing about his experience living with a disability in Japan. He encouraged me to expand and refine both our editing and submission processes so that we could reach out to even more contributors and platform their perspectives. The editorial team now consists of him and Rebecca Callahan, and they’ve had a hand in every article that TokyoDev has published. In other words, since 2022, Scott has shown that he cares about TokyoDev as much as I do. He is invested in what we do and why we do it, and shares my vision for what TokyoDev can become. What changes now Scott’s experiences as an engineer, a mentor, and a leader make me confident that, as our Head of Operations, he will continue to help TokyoDev mature. A big part of what has made TokyoDev successful is that I see the software developers who use our site as my peers and genuinely want to help them. Scott shares that perspective, and will join me in ensuring that TokyoDev remains true to its principles while we grow. So what changes in the short term? Nothing dramatic. I will teach Scott about managing TokyoDev as a business, and he will document our processes and products as a part of that learning. He will also look for areas of improvement while ensuring that we remain compliant with regulations and labor law. Over time, I will hand over some other responsibilities. I will continue to run TokyoDev, but TokyoDev will become less dependent on me for its day-to-day operations. Long term, I’m excited about what this will enable us to do. TokyoDev has always been about helping international software developers build successful careers in Japan, and bringing Scott on full time puts us in a much better position to continue doing that. We look forward to writing the next chapter of TokyoDev together.

yesterday • 1 votes
Speeding up Kidz Fun Art & making Google love it

Kidz Fun Art is a drawing, colouring and animation app for kids that I’ve been building since 2021. It runs in the browser also as a simple wrapper app on iOS, iPadOS and Windows. It wasn’t getting a great click through rate on Google, even for people who directly searched for it. So, for two days I took a hard look at how fast it loads and how search engines see it. It wasn’t flattering. This post covers what I found, what I changed and what the graphs say now. Short version: on a simulated slow phone, the homepage’s Largest Contentful Paint got about 2.6× faster (15.9 s to 6.1 s) and its first load is about 82% smaller (4.2 MB to 0.75 MB). I also fixed a set of SEO mistakes that are, frankly, embarrassing to write down. I’ll write them down anyway. Part 1: Google had no idea what the site was for I started by reading 16 months of Google Search Console data. 9,380 clicks in total. About 9,200 came from people who had typed “kidz fun art” or a close spelling. People searching for what the app actually does (“coloring app for kids”, “ai art for kids”, “make a comic book”) sent the site 164 clicks. In 16 months. Total. When there was a relevant page, Google ranked it well. The site was #1 for things like “drawing app for kids ipad” and a German query that means “paint by numbers app without ads”. There just weren’t many relevant pages. Then I looked at the code, and the cause was obvious. The homepage <title> was “Kidz Fun Art” in all 12 languages. That’s a fine name, and a terrible title. It tells a search engine nothing. The homepage’s first <h1> was the pricing table’s “Choose your experience”. I was, in effect, telling Google that the most important thing on my homepage was the checkout. Every blog post had the same meta description: a generic line about “news, updates, tips”. Each post’s front matter already had a perfectly good excerpt field that nobody read. Blog posts had two <h1>s: the site header and the post title. There was no structured data at all. The sitemap that redirected every single URL This one stings. The sitemap is the one file whose whole job is to say “here are my URLs, please index them”. Mine listed 17 URLs, and every one of them redirected. The sitemap build used an environment variable that resolved to kidzfun.art. The site lives on www.kidzfun.art, so every URL got a 308. Only the English blog posts were listed. The other 11 languages appeared only as hreflang alternates, which pointed at redirecting URLs too. There were no <link rel="canonical"> tags on any page and no hreflang in the page <head>. Google was left to guess which URL was the real one, and it guessed wrong a lot. What I changed: The sitemap is now built on https://www.kidzfun.art, with 204 URLs: every home page, blog index, blog post, About page and gallery page, in all 12 languages. Each one lists all of its language alternates plus an x-default. A shared SeoLinks component adds an absolute canonical (www, no query string) and the full hreflang set to every public page. The ?v= cache-busting URLs that had been showing up as duplicates now canonicalise to the clean URL. /xx/blog/yy/... URLs, which returned a 500 in production (oops), now 308 to /blog/yy/.... Login, account, debug, export and embed pages send X-Robots-Tag: noindex, follow from next.config.js, so they stop using up crawl budget. robots.txt now mentions the sitemap. Novel idea. Why Google skipped half the site Search Console’s Pages report had 93 of 174 known pages not indexed. Every reason on that list traces back to the mistakes above: “Page with redirect” (22): that sitemap, plus internal links to the non-www host. “Duplicate without user-selected canonical” (18): I hadn’t selected any canonicals. Fair cop. “Crawled, currently not indexed” (39): mostly translated posts that Google reached through redirecting URLs, plus thin pages like /login. “Discovered, currently not indexed” (12): Google found these and decided they weren’t worth a visit. With no sitemap entry and no hreflang, I hadn’t given it a reason to. On-page fixes Titles and descriptions: Each blog post got its own seoTitle and description, in all 12 languages. The meta description now comes from the post’s excerpt, which is what that field was for all along. The About pages take their title and description from their own localized Markdown. They had been sharing an English title and a generic description. One <h1> per page: A # heading inside a post’s Markdown now renders as an <h2>, so the post title is the only <h1>. The About page opts back in with a keepH1 prop. Its Markdown heading is the page title. This fix arrived in two parts. The first part left the About page with zero <h1>s. I noticed, eventually. Structured data and social tags: JSON-LD for the home page (the app), blog posts (articles) and the embedded videos. Open Graph and X/Twitter tags on every public page, so shared links get a proper card. A real 404 for deleted gallery images: The AI gallery shows a friendly “AI Artwork Not Found” page when an image has been deleted. It used to return 200 OK, which Google calls a “soft 404” and dislikes. It now returns a 404 with noindex, and a 503 if the Firestore lookup itself fails, so a database hiccup tells crawlers “come back later” rather than “this is gone”. Part 2: Making the homepage fast The homepage is the app. It works offline, so every dialog (save, stickers, colouring pages, “get an adult”, “no wifi”) is server-rendered into the HTML up front. That’s a deliberate trade-off, and I kept it because I want new users to be able to immediately be able to start drawing as soon as they launch the app. What I hadn’t thought about was what it meant for the network. What the homepage downloaded before the app could start I measured the live site with a Lighthouse-style mobile profile: slow 4G (1.6 Mbps, 150 ms latency), 4× CPU slowdown, empty cache. First load: 4.5 MB over 198 requests. The homepage HTML alone was 358 KB, with about 77 <img> tags in hidden dialogs. The browser dutifully fetched every one of those images. The script that starts the app (main.min.js, 191 KB) sat at the very end of the document, so the browser found it last and gave it whatever bandwidth was left. The app script finished arriving at about 14 seconds. The CPU was not the problem. Every long task was under 300 ms. This was purely about bandwidth and request order. I had built a very polite queue and put my own app at the back of it. Watch the queue The waterfall is a simplified illustration. The milestone times (first paint, LCP, load) are measured: slow 4G + 4× CPU, local production build, median of 3 runs. My Largest Contentful Paint was the “Contact support” link The canvas doesn’t count toward LCP, and the toolbar buttons are small. So on a slow load the largest thing painted was… the loading dialog. The loading dialog fades in its “Taking a while? Contact support” text after 5 to 7.5 seconds. On a slow connection, that text became the page’s LCP element, at 15.9 s. I was measuring my page speed by how long it took to suggest the user complain about my page speed. There’s a lesson in there somewhere. The fix wasn’t to change the dialog. It was to make the app start sooner, so it paints before that text ever appears. What I changed Preload the app script. I added <link rel="preload" as="script"> in <head> for main.min.js and errorHandler.min.js. The <script> tags stay where they were, so execution order doesn’t change. The browser just starts fetching them before any image. This one line did more than anything else on this list. It’s a little humbling. Turn the three dialog GIFs into MP4s. Three animated GIFs: an arrow pushing pictures into a tablet (927 KB), a dancing rainbow pencil (650 KB) and a paper plane (305 KB). That’s 1.9 MB of decoration in dialogs most visitors never open on their first visit. I re-encoded them with ffmpeg: H.264 High profile, -preset veryslow -crf 20, yuv420p, +faststart, constant frame rate taken from each GIF’s frame delay. The commands are committed as scripts/gif-to-mp4.sh. The result is 130 KB in total, with SSIM between 0.988 and 0.993 against the originals. The pencil needed tpad=stop_mode=clone:stop_duration=0.2, because ffmpeg’s GIF reader drops the final 250 ms hold and the dance lost its big finish. The new <video autoplay loop muted playsinline> tags render at exactly the same size and class as the old <img> tags. Alt text moved to aria-label. Locked colouring thumbnails were full-size PNGs. In the colouring page picker, locked pages used the full-size colouring PNG as their background image, inlined into the HTML. That came to about 1.3 MB on every first visit, to draw thumbnails in a dialog that was closed. They now use the small image, set when the picker opens, like the unlocked ones always did. Sticker thumbnails wait their turn. I tried loading="lazy" first. It didn’t help: Chrome fetched the images before the stylesheet that hides the dialogs had arrived, so as far as it knew they were visible. Sticker thumbnails and the media library’s YouTube thumbnail now use data-deferred-src. They load 3 seconds after the load event, or as soon as their dialog opens, whichever comes first. The service worker preload starts after load. The offline preload queue (about 2 MB of colouring pages and stickers) used to start while the page was still loading and compete with it. Now it starts after the load event. Same files, same offline result, no fighting for bandwidth. Analytics is async. Nothing waited for it, so nothing needed to block on it. Cache headers. /images/*: max-age=86400, stale-while-revalidate=604800. It had been max-age=0, so every image was re-checked on every visit. Versioned /js, /css and /addToHomescreen files are immutable for a year in production. Offline still has to work Offline support is a core feature, so “just don’t download it” was never an option. Some of the changes above put it at risk, and a couple of things turned up along the way: Safari plays video from byte ranges. Safari requests video with Range: bytes=.... Answer that with a full cached response and the video won’t play. The service worker now answers Range requests from its cache with a proper 206 Partial Content slice (lib/util/rangeResponse.ts, with unit tests). First-visit gap: on a first visit the service worker isn’t controlling the page yet, so dialog images only reached the offline cache on the second online visit. The page’s own dialog images and videos now go to the front of the offline preload queue on the first visit. I nearly downloaded my whole site, 12 times over. The service worker cached every <link href> it found in the page. The new SEO work had just added canonical and hreflang <link>s pointing at all 12 localized homepages. So every new visitor would have downloaded about 12 extra homepages, in languages they don’t speak, “for offline use”. I caught it before production. The service worker now skips rel=canonical and rel=alternate. Safari stopped the service worker mid-download. The old preload queue ran on timers after the request had been answered. Safari considers an idle service worker fair game, so it stopped it after the first few images. The page now sends images in batches of 10, and the service worker caches each batch before it replies. A page lock-up I’d never noticed. resizeDialog added a new “image loaded” listener every time the window resized. With lots of images loading at once, the listeners multiplied until the page locked up. It now attaches one listener per image, which, in hindsight, is how many it needed. Part 3: The blog Blog index cards used the full 1920 px screenshots as covers, up to 500 KB each, in a box about 200 px wide. Each card was a little billboard, shrunk down by the browser on every visit. Covers now go through next/image with sizes that match the actual card widths, so the browser gets a resized WebP. The first few cards get priority. The rest load lazily. The first sizes hint was too small at some widths and made covers blurry. It now matches the real card widths, up to about 630 px between 769 px and 1028 px wide. Post pages: The cover image gets fetchpriority="high", and images further down the post load lazily. Five images were recompressed in place, keeping the same filenames and dimensions. The PNGs came out about 4× smaller and the JPEGs about 40% smaller. I deleted a 1.1 MB cover.png that nothing referenced. All 12 translations of that post used cover.jpg. The PNG had just been sitting in public/ taking up space. Results on the slow 4G profile: /blog LCP went from 13.3 s to 1.8 s. A typical post went from 3.9 s to 1.7 s. The gallery didn’t get faster, on purpose. It got a 5-minute edge cache and a cached Firestore image count, which fixed its slow server responses. I decided not to change its 20 full-size PNG thumbnails, so its lab LCP barely moved (8.4 s to 8.6 s). That’s a choice, and the chart owns it. Part 4: Lighthouse homework After the speed work went live I ran Lighthouse on the production homepage (mobile) and cleaned up what it flagged. Performance stayed in the low 70s, so I have some more work to do, but it’s moved nicely in the right direction. Part 5: Telling the search engines Fixing the site is half the job. The other half is getting crawlers to come back and notice. Google Search Console: Resubmitted the www sitemap. Clicked “Validate fix” on “Duplicate without user-selected canonical” and on the redirect errors. Requested indexing for the main language homepages and the top blog posts. The quota is about 10 a day, so it’ll take a few days. Wrote down a baseline from the Performance report, so in 4 to 8 weeks I can tell whether any of this worked. Bing Webmaster Tools: Submitted all 204 sitemap URLs through URL Submission. The daily quota is 10,000, so no trickle-feeding needed. Bing’s index also feeds DuckDuckGo and several AI search tools, so it’s cheap extra reach. Most of Bing’s “SEO recommendations” pointed at old ?v= URLs and old titles. They’ll clear as Bing recrawls. What I’m not watching: the Lighthouse score. Google ranks on real-user Chrome data (the Chrome UX Report): a rolling 28-day window, judged at the 75th percentile. So the honest answer to “did it work?” is “ask me in a month”. How I kept from breaking things The app has hundreds of unit and integration tests, all of which still pass (even though they do take over 30 minutes to run). I also manually regression tested all the riskiest changes. Hopefully I don’t make things worse before they get better… Lessons, mostly at my own expense Read your own sitemap. Actually open it and click a URL. Mine had been quietly redirecting for who knows how long. <link rel="preload"> for the script that boots your app is the cheapest big win there is. Browsers fetch resources in the order they find them. If your app script is at the bottom of a 358 KB document full of images, it really is last in line. Check what your LCP element actually is before you try to make it faster. Mine was a “Contact support” link. GIF is an image format from 1987 that I was using as a video format. A real video codec beats it by an order of magnitude at the same quality. try/catch doesn’t catch promise rejections. I knew that. My code didn’t. Every <link> you add to <head> is something your service worker might decide to download. Speed scores are for diagnosis. Rankings come from real users. Fix what the lab finds, then wait for the field data. Epilogue You’re still here? Wow. Cool. After all this, I’m hoping that users notice nothing other than quicker app start times. I’m looking forward to keeping an eye on traffic and retention in the coming weeks. Wish me luck!

yesterday • 1 votes
Why we're moving our warehouse off DuckDB

Five months ago, we rebuilt our warehouse on DuckDB instead of ClickHouse. We did it because a shared ClickHouse cluster couldn't handle hours-long…

yesterday • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in