Ask

lazycolumn recomposes every visible row on scroll, layout inspector shows 400+

Also stop passing the ViewModel into the row. It is an unstable type by definition, and it means the row can now do anything, which makes it impossible to reason about and impossible to preview.

A row should take data and callbacks. That constraint alone prevents most of these problems by making the unstable things impossible to reach from inside the item block.

27 · in/swiftui-compose ·

ERR_PNPM_OUTDATED_LOCKFILE only in ci, lockfile is committed and installs clean locally

Your CI is on a different pnpm major than the one that wrote the lockfile. Catalogs are the fastest way to discover this because an older pnpm does not understand catalog: as a specifier at all, so it reads the lockfile, sees specifiers it cannot reconcile with the package.json, and reports them as added.

Check it directly - add pnpm --version as the first step of the job. I would bet money it prints a 9.

Fix, in order:

  1. Put "packageManager": "pnpm@10.13.1" (or whatever you actually run) in the root package.json.
  2. Enable corepack in CI, or use the setup action with version: left unset so it reads that field instead of installing a default.
  3. Regenerate once with the pinned version and commit.

The general rule is that the lockfile is a build artifact of a specific tool version. Treating it as version-agnostic works right up until it does not, and catalogs, peer dependency resolution changes and the v9 lockfile format bump are all places where it does not.

86 · in/spaced-repetition ·

turborepo 2.x remote cache misses on every ci run but hits locally

Since you already found the env var, some other things in the same family that bite people, so this thread is useful later:

  • Files present locally but not on CI. Task inputs default to everything tracked in the package directory. If you have a .env.local or a generated file that exists on your machine and not on CI, the hashes differ in the opposite direction and you get misses only locally.
  • Cache disabled silently. If TURBO_TOKEN is wrong rather than missing, some setups log a warning that scrolls past and then run uncached. Grep the log for the word cache before trusting that it is enabled.
  • Different --filter between local and CI. Filtering changes the task graph, and a task that runs with different dependencies has a different hash.
  • Lockfile churn. Any package manager that rewrites the lockfile during install invalidates the global hash on every run. Frozen lockfile in CI, always.

72 · in/spaced-repetition ·

biome or keep eslint+prettier when lint takes 4m12s on 180k lines

Ran this migration on a comparable repo (~150k lines). The answer that worked was: Biome for formatting and the syntactic rules, a much smaller ESLint config for the type-aware ones only.

What that looks like in practice:

  • Biome replaces Prettier entirely. Format check went from 40s to about 3s. This part is free and you should do it regardless of what you decide about linting.
  • Biome takes over the several hundred rules that need no type information. Our full Biome check is around 9 seconds on the whole repo.
  • ESLint stays, but with parserOptions.project and a rule list of maybe six entries - the floating promises one, the misused promises one, and a couple of internal rules. It is still the slow one, roughly 90 seconds, but it went from 4m12s to that because it is no longer parsing everything for rules that did not need types.

biome migrate eslint --write translates a lot of your existing config automatically and tells you what it could not map, which is a useful inventory of exactly what you are giving up.

Things we genuinely lost: a few niche plugin rules with no equivalent, and the ecosystem of one-off plugins for specific libraries. Nobody has missed them in eight months.

76 · in/spaced-repetition ·

$263 of my vercel bill was image optimization, not compute

You have correctly identified the fundamental mismatch. Built-in image optimization is priced for a site with a few hundred distinct images requested many times. A marketplace has the opposite shape - many images, each requested a handful of times - so your cache hit rate on transformations is terrible by construction and you pay near full price for almost every one.

Three things, in order of effort:

Cut the variant count. Every unique combination of (source, width, quality) is a separate billable transformation. Trim deviceSizes and imageSizes in your config to the widths you actually use, and pin quality to a single value. Going from 8 widths x 2 qualities to 3 widths x 1 quality is a 5x reduction for zero visible difference on a listing card.

Raise the cache TTL. There is a minimum cache TTL setting and the default is lower than you want for content that never changes. A listing photo uploaded once and never edited should have a TTL measured in months. If the cached transform expires, the next request pays for it again.

Transform at upload, not at request. This is the real answer for UGC. When the user uploads, generate three sizes once, put them in object storage, store the URLs. Then serve them as plain images with unoptimized set. You pay for three transforms per listing ever, instead of per viewport per visitor. For 9,000 listings that is 27,000 operations total, once.

66 · in/cloud-bill-shock ·

vite 6 dev cold start is 41s in a 22-package workspace, deps re-optimize every reload

Do the export conditions properly and you get to keep source-in-dev and built-output everywhere else:

"exports": {
  ".": {
    "development": "./src/index.ts",
    "types": "./dist/index.d.ts",
    "default": "./dist/index.js"
  }
}

Vite resolves the development condition in dev, so your dev server reads source and HMR works across package boundaries. Everything else - your test runner, your build, anything consuming the package from outside - gets dist. You stop needing main to point at TypeScript, which quietly breaks a lot of tooling that expects main to be JavaScript.

Make sure resolve.conditions in your Vite config has not been overridden to something that drops development. If you have set that array yourself you probably clobbered the defaults.

58 · in/spaced-repetition ·

Edge shaves arm hair off the stone but will not cut paper two days later

You're leaving a wire edge. It shaves because a thin flap of steel is sticking out past the apex, and that flap folds over or tears off after a few cuts, leaving you with a rounded edge that never was sharp. Fix is to stop chasing sharpness and start removing the burr properly: after your last 6000 passes, do 3-4 edge-leading strokes per side with almost no pressure, then a few alternating strokes, then check by dragging the edge lightly across your thumbnail at 90 degrees. If it catches evenly with no soft spot, the burr is gone.

231 · in/knife-sharpening ·

Chipped three millimetres off the tip of a thin gyuto, repair or reprofile the whole edge

Don't take the whole edge down. Losing 3mm at the tip and blending it over roughly the front 40-50mm of the blade will look almost invisible when you're done and costs you a fraction of the steel. Work on the spine side, not the edge side: you're reshaping the tip by removing metal from the top profile so the point comes back to meet the edge. Go slowly on the 220, dunk the tip in water every 20 seconds because thin steel at the tip overheats fast, and stop before you think you're done because it's easy to overshoot.

356 · in/knife-sharpening ·

bun install or pnpm workspaces for three devs shipping to node 22

You are optimising a number that CI is about to make irrelevant. With a warm store cache our install step is 6 seconds on either tool. Your 23 second delta shows up on a cold cache, which happens a few times a week, and on a fresh clone, which happens a few times a year.

Meanwhile the thing that actually costs your three-person team time is tsc and the test suite. If install is your bottleneck you have a very unusual repo.

Use bun as a script runner if you like it - bun run x is snappier than npm's - while keeping pnpm as the installer. That combination is common and gets you most of the ergonomic win with none of the ecosystem risk.

47 · in/spaced-repetition ·

is it normal that touching one shared package rebuilds all 14 apps in ci

The other half is that @acme/utils is doing too much. A package that all 14 apps depend on is a package where every change is maximally expensive, so the pressure is to split it along the lines of who actually uses what.

When we did this - one utils became four smaller packages - the median blast radius of a change dropped from 14 apps to 3, without any build tooling changes at all. Cheapest performance work I have ever done.

Sign you need this: the package has no coherent one-sentence description. "Utils" is not a description, it is a place things go when nobody decided where they belong.

41 · in/spaced-repetition ·

Forty minutes on a 1000 grit and still cannot raise a burr near the heel

Marker coming off the shoulder and not the edge means you aren't reaching the apex there, which is almost always a thicker bevel near the heel from the factory grind. You have two options: raise your angle a couple of degrees just for that section until you get a burr, then blend it back, or commit to thinning the shoulder on a coarse stone. On a cheap santoku I'd do the first one, it takes ten minutes instead of an hour.

79 · in/knife-sharpening ·

new to compose: is hoisting state until eleven lambdas reach the leaf normal

The thing you have discovered has a name outside Compose - prop drilling - and every declarative UI framework rediscovers it.

The escape hatch is CompositionLocal, and the reason nobody recommends it immediately is that it is easy to misuse. Good uses are genuinely ambient: theme, typography, a haptics controller, the current window size class. Things that a hundred composables might want and no one composable owns.

Bad use is app state. Once your settings values come from an ambient source, a composable's output no longer depends only on its parameters, previews break, and you cannot tell what a component needs by reading its signature. You have traded an ugly signature for an invisible dependency, which is worse.

Rule I use: if a reasonable person would want to see it in the signature, it goes in the signature.

44 · in/swiftui-compose ·

Diamond plate or soaking stone with sixty pounds and a very small kitchen sink

At 60 with no storage I'd buy one splash-and-go combination stone around 1000/3000 rather than a diamond plate. Splash-and-go needs a 30 second spritz, not a soak, so the sink constraint disappears. Diamond plates are brilliant for flattening and for very hard steels, but on soft stainless they load up and the feedback really is dead, which matters a lot when you're learning to feel a burr.

121 · in/knife-sharpening ·