Tools
I Hacked GitHub Readmes to Show Real-Time Data (No Spinner, No JS)
2025-12-30
0 views
admin
💀 The "Spinner of Death" ## 💡 The "Refresh" Header Hack ## 🛠️ The Build (Node.js + Canvas) ## ⚡ Why This Approach is Stupidly Efficient ## ⚠️ The "It Works on My Machine" Trap ## 🚀 Optimization: Don't DDOS Yourself ## 🔮 The Potential: What else can you build? ## 📦 Try it out So, here’s the scenario: I wanted my GitHub profile (README.md) to feel alive. Not just those static "Stats" cards everyone has, but something truly real-time. Maybe a live Bitcoin ticker, a "currently playing" Spotify track, or just a clock that actually ticks. I thought, "Easy. I'll just stream a GIF." I spun up a Node.js server, set up a multipart stream, and... it worked! But there was one massive, annoying problem. When you stream a GIF (or MJPEG) to a browser, the request never ends. The browser thinks the file is still downloading (because it is), so the tab’s favicon turns into a permanent loading spinner. If you put this on your GitHub profile, the moment someone lands on your page, their browser looks like it's struggling. It feels janky. It feels broken. I refused to accept that. I wanted the live updates, but I wanted the browser to think the request was finished. I went down a rabbit hole of old-school web protocols and found a relic from the Netscape era: the Refresh header. Instead of keeping the connection open forever (Streaming), we do this: Server generates one single frame. Server sends that frame with a specific header: Refresh: 1. Server closes the connection immediately. The Magic: The browser receives the image, stops the loading spinner (because the request is done!), and then—obediently—waits 1 second and requests the URL again. Visually? It looks like a 1 FPS video. Technically? It’s a series of discrete, finished HTTP requests. Zero loading spinner. I kept the stack simple: Express for the server, node-canvas to draw the pixels, and gifencoder. Here is the core logic. Notice we aren't using setInterval anymore. We just draw once per request. When I first built this, I worried about performance. Is making a new HTTP request every second bad? Actually, for this use case, it's more scalable than streaming. If you try to deploy this, you will hit a wall. I learned this the hard way. node-canvas isn't just JavaScript; it binds to C++ libraries like Cairo and Pango. If you deploy this to a fresh Ubuntu VPS or a lightweight Docker container, it will crash and burn because those system libraries are missing. I added a specific "System Requirements" section to my repo because of this. You need to install libcairo2-dev and friends before npm install will even work. (Don't worry, I put the exact commands in the README so you don't have to StackOverflow it). The problem with the Refresh approach is that if 1,000 people view your GitHub profile, your server gets 1,000 requests per second. Rendering a Canvas 1,000 times a second will melt your CPU. I added a simple TTL Cache. If a request comes in and the last image was generated <900ms ago, I just serve the cached buffer. Now, my server only does the heavy lifting once per second, regardless of how many people are watching. This isn't just for Bitcoin prices. Because this output is a standard image, it works in places where JavaScript is banned—like Email Clients and Markdown files. Here are a few "efficient" ideas you could build with this: I open-sourced the whole thing. It’s a clean boilerplate—you can clone it, change the drawing logic to show your own stats (or just a generic "Matrix" rain), and deploy. Repo: https://github.com/sunanda35/Infinite-GIF If you find it cool, drop a generic ⭐️ on the repo. It feeds my dopamine. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
app.get('/live-status.gif', (req, res) => { // Setup Canvas const canvas = createCanvas(400, 150); const ctx = canvas.getContext('2d'); // Draw your "Hacker" UI ctx.fillStyle = '#0d1117'; // GitHub Dark Bg ctx.fillRect(0, 0, 400, 150); ctx.fillStyle = '#0f0'; // Matrix Green ctx.font = '24px Monospace'; ctx.fillText(`BTC: $${getCurrentPrice()}`, 20, 50); // The Magic Header // "Refresh: 1" tells the browser to reload this URL in 1 second res.set({ 'Content-Type': 'image/gif', 'Cache-Control': 'no-cache', 'Refresh': '1' }); // Send & Close encoder.start(); encoder.addFrame(ctx); encoder.finish(); res.send(encoder.out.getData());
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
app.get('/live-status.gif', (req, res) => { // Setup Canvas const canvas = createCanvas(400, 150); const ctx = canvas.getContext('2d'); // Draw your "Hacker" UI ctx.fillStyle = '#0d1117'; // GitHub Dark Bg ctx.fillRect(0, 0, 400, 150); ctx.fillStyle = '#0f0'; // Matrix Green ctx.font = '24px Monospace'; ctx.fillText(`BTC: $${getCurrentPrice()}`, 20, 50); // The Magic Header // "Refresh: 1" tells the browser to reload this URL in 1 second res.set({ 'Content-Type': 'image/gif', 'Cache-Control': 'no-cache', 'Refresh': '1' }); // Send & Close encoder.start(); encoder.addFrame(ctx); encoder.finish(); res.send(encoder.out.getData());
}); COMMAND_BLOCK:
app.get('/live-status.gif', (req, res) => { // Setup Canvas const canvas = createCanvas(400, 150); const ctx = canvas.getContext('2d'); // Draw your "Hacker" UI ctx.fillStyle = '#0d1117'; // GitHub Dark Bg ctx.fillRect(0, 0, 400, 150); ctx.fillStyle = '#0f0'; // Matrix Green ctx.font = '24px Monospace'; ctx.fillText(`BTC: $${getCurrentPrice()}`, 20, 50); // The Magic Header // "Refresh: 1" tells the browser to reload this URL in 1 second res.set({ 'Content-Type': 'image/gif', 'Cache-Control': 'no-cache', 'Refresh': '1' }); // Send & Close encoder.start(); encoder.addFrame(ctx); encoder.finish(); res.send(encoder.out.getData());
}); CODE_BLOCK:
if (lastBuffer && (Date.now() - lastGenerated) < 900) { return res.send(lastBuffer); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
if (lastBuffer && (Date.now() - lastGenerated) < 900) { return res.send(lastBuffer); } CODE_BLOCK:
if (lastBuffer && (Date.now() - lastGenerated) < 900) { return res.send(lastBuffer); } - No Open Connections: In a streaming architecture, if 5,000 people view your profile, your server holds 5,000 open socket connections. That eats memory fast. With the "Refresh" hack, the server sends the data and immediately forgets the user. It’s stateless.
- The "O(1)" Cache: I added a simple TTL Cache. If 1,000 requests hit the server in the same second, I only draw the canvas once. The other 999 users just get a copy of the memory buffer.
- Low Memory Footprint: Since we aren't managing unique user states or long-lived streams, this server can run comfortably on a tiny 512MB VPS or a free-tier container. - Dynamic Email Signatures: A banner in your footer that shows your latest blog post title or your company’s current uptime status.
- The "Hiring" Badge: A badge on your repo that turns Green when you are "Open to Work" and Red when you are busy, synced to your calendar.
- Event Countdowns: A "Time until Launch" clock that embeds directly into your newsletter.
- Spotify Visualizer: Connect to the Spotify API to show what you are listening to right now on your profile.
how-totutorialguidedev.toaiubuntuserverdockernodejavascriptgitgithub