One Dev's Revenge on Slow Placeholder Services: A Single Chinese CDN for Every Frontend Asset
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:
- Placeholder color blocks → placehold.co
- User avatars → DiceBear's API
- List fake data → JSONPlaceholder
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:
- Small services set up by individuals — usable, but might disappear any day;
- Ones that only do a single function — placeholder images are placeholder images, avatars still need to be found elsewhere;
- 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.
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.
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.
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.
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.
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.
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:
- Scene images are currently still text SVGs, not illustrations. Just a color scheme with a line of title — usable, but not good-looking. The illustrated version is in progress (any friends skilled in design, contact me and help me out?).
- The photo library is still expanding. Currently it's a curated CC0 pack, not a large quantity — some niche subjects repeat after a few pages. I'll strive to keep updating, but for the development phase, the current quantity is definitely enough and won't be exhausted.
- Mock only has three resource types, and is read-only.
POST/PUT/DELETEaren't supported yet, and nested resources (like comments on articles) aren't there either. My main energy hasn't been on this part, but I have quite a big blueprint for mock data — just that doing it alone takes a long time, please look forward to it. - One person doing it. Frontend, backend, operations, documentation are all me alone, so the update pace depends on whether I'm working overtime lately or playing games. (Want to work on it together?).
- Striving not to disappear. If few people use it, my server resources are very ample and I'll keep operating. If many people use it and scaling is needed, an API key method might be introduced to sustain it.
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.
- Website: https://devimg.cn
- CDN: https://cdn.devimg.cn
- 🐧 Feedback group: 166188735
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.