跪拜 Guibai
← Back to the summary

One Dev's Revenge on Slow Placeholder Services: A Single Chinese CDN for Every Frontend Asset

image.png

Recently, I was writing a frontend component library. The task itself wasn't complicated — after writing the components, I needed to add matching examples for them.

The trouble was with the images.

Components like Card, Avatar, Image, List... without images in the examples, you can't see the effect at all. A plain colored div with "this is an image" written on it makes people say directly: "What's the difference from not having done anything?"

So I found something fairly usable: picsum.photos.

<img src="https://picsum.photos/800/600" />
<img src="https://picsum.photos/seed/card-1/400/300" />

It ran fine on my local machine — because I had a proxy on.

The real problem was one day when I forgot to turn on the proxy, and the component example page opened completely blank, spinning for ages.

Each example had one or two images. In the Network panel, a whole column of picsum requests were queued up pending. The slowest one took 8 seconds to come back, and three or four others timed out and turned red directly. The entire example page, from clicking in to all images appearing, took 12 seconds.

🥚疼!

It got worse and worse

This matter wasn't big originally, but driven by a spirit of exploration, I discovered:

First, it's not just slow — it's unstable. Slowness can be tolerated; the most disgusting thing is being fast sometimes and slow other times. Refreshing the same page three times gave three different layouts, because some images loaded and some didn't, and the page height kept jumping.

Second, CI directly crashed. My component library ran visual regression tests, using Playwright to take screenshots and compare them with baseline snapshots. The images came from a third party and were random, so every run produced different screenshots, and the snapshot comparisons were all red. I initially thought my components were broken and checked for a long time, only to finally discover it was the images' fault. I was gritting my teeth at that moment. 🥚疼!*2

Third, for one placeholder image requirement, I pulled in three external domains. Everyone should be familiar with this combo:

Three services, three domains, all on foreign networks. Speed aside, their path styles, parameter naming, and caching strategies were all different from each other — I had to remember three sets of APIs at the same time.

Fourth, the fake data was all in English. JSONPlaceholder returned Leanne Graham, [email protected], Gwenborough. What I needed to verify was Chinese name line breaks, ultra-long Chinese title truncation, and Chinese addresses displayed on narrow screens. Using English data to test Chinese typography was equivalent to not testing at all. After going live, problems indeed appeared: two-character names and four-character names on the same line didn't align. 🥚疼!*3

During that period, I searched for a long time for domestic alternatives. What I found fell roughly into three categories:

  1. Small services set up by individuals — usable, but might disappear any day;
  2. Ones that only do a single function — placeholder images are placeholder images, avatars still need to be found elsewhere;
  3. Some that require registration, applying for a Key, and carrying a token in request headers;

I just wanted to write an address in <img src>, not pull in an SDK for this. 🥚疼!*4

After searching around and finding nothing convenient — well, if nothing works, I'll just write it myself!

Just do it

➡️ Here it is: https://devimg.cn

I'll explain this website to you in the most direct, most straightforward, least verbose, least roundabout way: All the temporary assets that domestic developers need when writing pages are under the same domain name, directly written into <img src> and ready to use.

No registration, no Key required, no SDK.

The first principle I set for myself was: This thing must be faster than checking documentation. Any design that requires "install first, register first, read docs first" was cut. So the final form is just a URL.

The second principle was: The same URL must be able to return the same image forever. All interfaces with seed use Cache-Control: immutable. You can take a hundred snapshots and get the same one every time.

How to use

Copy the addresses below into your browser's address bar to see the effect — no preparation needed.

Placeholder Images

The most basic usage: just give width and height, done.

<img src="https://cdn.devimg.cn/800/600" />
<img src="https://cdn.devimg.cn/400/300?text=Banner&bg=409eff&fg=ffffff" />

If you need the image to stay fixed, use seed — the same seed always gives the same color scheme:

<img src="https://cdn.devimg.cn/seed/my-app/800/600" />

If you previously used placehold.co, that 800x600 style and path color scheme are also recognized:

<img src="https://cdn.devimg.cn/800x600?text=Banner" />
<img src="https://cdn.devimg.cn/800/600/409eff/ffffff" />

Returns SVG by default — small file size, doesn't blur when scaled. For mini-programs that must have bitmap images, add a .webp / .png suffix.

image.png

Avatars: Supports Chinese Initial Character

This is the feature I personally wanted the most. DiceBear's styles are very nice-looking, but it doesn't recognize Chinese — putting in 张三 comes out blank or garbled.

<img src="https://cdn.devimg.cn/avatar/devimg/张三/128" />
<img src="https://cdn.devimg.cn/avatar/lorelei/Luna/128" />
<img src="https://cdn.devimg.cn/avatar/devimg/张三/128?bg=3b5bdb&fg=ffffff&shape=square" />

Besides the self-made Chinese initial character avatars, I also integrated the open-source styles of DiceBear, Jdenticon, and Minidenticons — over 50 types in total. The same style plus the same name always gives the same face — especially convenient when making user lists, no need to maintain an avatar mapping table yourself.

If there are too many styles to remember, GET /avatar/styles returns the full directory with license information for each style.

image.png

image.png

Real Photos: Fetch by Use Case, Not by ID Lottery

picsum's photos are beautiful, but they're random. If I want to make an e-commerce product card and draw a forest landscape, that demo is wasted.

So the photo interface fetches by use case:

<img src="https://cdn.devimg.cn/photo/400/400?scene=product&seed=p-1" />
<img src="https://cdn.devimg.cn/photo/1200/400?scene=banner&seed=hero" />
<img src="https://cdn.devimg.cn/photo/640/480?cat=美食&seed=food-1" />

scene currently has over a dozen types: product for goods, food for dining, news for information, travel for trips, hotel for accommodation, banner for carousels, education for courses, realestate for properties, etc. For e-commerce lists, pass product; for news feeds, pass news — the images that come out match your business context.

With seed, it's fixed; without, it's random each time — choose based on your scenario.

image.png

Skeleton Screens and Empty States (Under continuous development, current effects don't quite meet requirements)

These two things — I used to hand-write SVGs or piece them together with divs, taking over ten minutes each time, and if the size changed, I had to redo them.

<img src="https://cdn.devimg.cn/skeleton/350/120?type=card" />
<img src="https://cdn.devimg.cn/skeleton/800/600?type=grid&cols=3&animate=1" />
<img src="https://cdn.devimg.cn/404" />
<img src="https://cdn.devimg.cn/scene/empty?theme=dark&title=购物车是空的" />

Skeleton screens have four layouts: page / card / row / grid, supporting light/dark themes and shimmer animation. Empty states have four types: 404, empty data, network disconnected, no search results — titles and subtitles can all be changed to Chinese.

image.png

Pseudo QR Codes

This feature was added on a whim. Mainly for QR code placeholders — using a real QR code generation library to generate one, I was afraid someone might actually scan it with their phone, so I made something that looks like a QR code but isn't one.

<img src="https://cdn.devimg.cn/qr/checkout/128" />
<img src="https://cdn.devimg.cn/barcode/sku-mock/320/80" />

What's generated are patterns that look like QR codes and barcodes, unscannable, with X-DevImage-Pseudo-Code in the response header to mark their identity. Purely for placeholder use, no one will misuse them.

image.png

Chinese Mock Data

The path style follows JSONPlaceholder, with the prefix changed to /mock:

const users = await fetch('https://cdn.devimg.cn/mock/users?_page=1&_limit=10').then(r => r.json());
const post = await fetch('https://cdn.devimg.cn/mock/posts/1').then(r => r.json());

What's returned is Chinese data — names are "张三", addresses are "上海", article titles are proper Chinese sentences. Three resource types: users, articles, products — 100 items each. The same id returns the same content every time.

Incidentally, the avatar, cover, image fields in the response themselves point to devimg's image interfaces, so when you fetch /mock/posts once, the data you get can directly render a complete list with cover images, no need to supplement images yourself.

image.png

Migrating from picsum / placehold is basically just changing the domain name

This is where I spent the most effort on compatibility. No one wants to do a global refactor just to change an image host for code already written in a project.

What you originally wrote Replace with
picsum.photos/800/600 cdn.devimg.cn/photo/800/600
picsum.photos/seed/x/800/600 cdn.devimg.cn/seed/x/800/600
picsum.photos/id/1/800/600 cdn.devimg.cn/id/1/800/600
placehold.co/800x600 cdn.devimg.cn/800x600
jsonplaceholder.../users cdn.devimg.cn/mock/users

One thing to note: in devimg, /800/600 returns a synthetic color block, not a photo. If you want a photo, you need to go through /photo/800/600. This is intentionally separated — synthetic images and real photos are fundamentally two different needs; mixing them together means you can't control what you get.

A few points I'm quite satisfied with myself

All default to SVG. Placeholder images, avatars, skeleton screens, scene images, code-shaped placeholders are all server-side synthesized SVGs — a few KB each, no need to wait for image decoding, no blurring when scaled arbitrarily. If you really need a bitmap, add a .webp suffix for transcoding.

Seed consistency runs throughout. Placeholder images, avatars, photos, QR codes all support seed — same parameters guarantee the same result, and the response header is max-age=31536000, immutable. CI visual regression, UI walkthroughs, design reviews — none will encounter "the image changed" anymore. This is the design I insist on most after having stepped in that pit.

No registration, no Key, no SDK. Ready to use right away. Default limit is 1000 requests/minute/IP, raster transcoding 60 requests/minute/IP — more than enough for normal development.

Deployed domestically. Tencent Cloud + COS + CDN — this is the entire reason this project exists. All the features above, other services have more or less, the only difference is you don't have to wait when opening.

Chinese first. Avatars recognize Chinese initial characters, Mock data is in Chinese, photo subjects are labeled in Chinese, scene image text can be changed to Chinese. These details are very small individually, but put together, you'll be much more comfortable when testing Chinese typography.

By the way

Some things aren't finished yet — let me be clear upfront to avoid everyone coming with expectations and leaving disappointed:

Finally

This project initially started purely to solve my own problem — making component library examples open within one second without a proxy. As I worked on it, I found quite a few problems that could be solved along the way, and it gradually became what it is now.

If you're also being annoyed by the same things, or if you have features you want but aren't there yet (icons? Lottie? More Mock resources?), feel free to suggest them.

I'm basically always in the group — tell me directly if there are bugs, say directly if you have requests, I'll schedule what I can.