DEV Community

Cover image for I Built a Browser From Scratch, and It Finally Renders the World's First Website Like Chrome Does
Nitin Kumar Yadav
Nitin Kumar Yadav

Posted on

I Built a Browser From Scratch, and It Finally Renders the World's First Website Like Chrome Does

A while back I set myself a slightly unhinged goal: build a web browser from scratch in Node.js and Electron no external HTML/CSS/layout libraries, everything hand-rolled. URL parser, TCP/TLS socket, HTTP pipeline, HTML tokenizer, DOM builder, CSS tokenizer, CSS parser, style matcher, layout engine, canvas renderer. All of it, from zero.

so,I called it Courage Browser.

This week, after dozens of daily sessions, I hit a milestone that felt disproportionately satisfying: Courage now renders info.cern.ch the very first website ever put on the internet almost pixel-for-pixel identical to real Chrome.

It sounds small. It is not small. Getting there meant chasing down bugs across nearly every layer of the browser.

Why info.cern.ch

If you haven't seen it, info.cern.ch is CERN's preserved copy of Tim Berners-Lee's original website. It's about as simple as HTML gets — one heading, a paragraph, a bulleted list of links. No CSS file, no JavaScript, no styling of any kind beyond what a browser applies by default.

Which is exactly why it's a great test case. If your browser can't get a page with zero author CSS to look right, it has no business trying to render anything more complex. Default styling headings being bold, links being blue and underlined, bullets showing up in the right place has to work before anything else does.

The bugs I found by just... comparing screenshots

I put a screenshot of Courage's render side-by-side with Chrome's and started listing differences. Two jumped out immediately:

  1. The <h1> wasn't bold in Courage, even though it clearly should be.
  2. The links had underlines but weren't blue , they were rendering in the default text color. Neither of these had anything to do with what I was originally working on that day (CSS attribute selectors, for an upcoming GitHub-rendering push). But they were visible, they were wrong, and they were small enough to fix in one sitting. So I did.

Bug #1: styles computed before they were applied

Courage has a defaultRules array the browser's own built-in stylesheet, the equivalent of what Chrome ships internally so that <h1> is big and bold even with no author CSS at all:

const defaultRules = [
    { selector: 'h1', declaration: { 'font-size': '32px', 'font-weight': 'bold', ... } },
    // ...
];
Enter fullscreen mode Exit fullscreen mode

These get applied by a function called styleMatcher, which walks the DOM tree and attaches matching declarations to each node's .styles object.

Separately, I have a getComputedStyle step that resolves things like CSS custom properties (var(--foo)) and snapshots the final resolved styles onto node.computedStyles, which is what the renderer actually reads from.

The bug: in my pipeline, I was taking that computedStyles snapshot before calling styleMatcher with the default rules for pages with no external or inline CSS (which is exactly info.cern.ch's situation). So every node's computedStyles got frozen as an empty object, then the default rules got applied to .styles too late for anyone to notice.

And because my renderer had this line:

const nodeStyles = node?.computedStyles || node?.styles || {};
Enter fullscreen mode Exit fullscreen mode

An empty object {} is truthy in JavaScript. So nodeStyles always resolved to the empty snapshot and never fell through to the correctly-populated .styles object, even though the right data was sitting right there, one property over, unused.

The fix was a one-line reorder: apply default rules before taking the computed-styles snapshot, not after. Embarrassingly simple once I saw it. Finding it took actually looking at the two renders side by side and asking "what's specifically different" instead of assuming everything was fine.

Bug #2: color was only checked one level up

The link-color bug was a similar shape. My default rule for <a> tags only set text-decoration: underline no color at all, so I added the standard browser default blue, #0000EE.

But that alone didn't fix it. My renderer's fill color logic was:

ctx.fillStyle = parentStyles.color || '#333333';
Enter fullscreen mode Exit fullscreen mode

It only ever checked the parent node's styles for color, never the node's own styles. Since I'd just set color directly on the <a> element itself, it was being completely ignored. Fix:

ctx.fillStyle = nodeStyles.color || parentStyles.color || '#333333';
Enter fullscreen mode Exit fullscreen mode

Check the node's own resolved styles first, and only fall back to the parent if nothing's there.

The result

Bold heading. Blue, underlined links. Bullets in the right spot. Side by side with Chrome, it's genuinely hard to tell which screenshot is the from-scratch browser and which is the industry-standard one, at least for this page.

That's the whole point of building something like this incrementally every bug you fix isn't just "one page looks better now," it's a general correctness fix that every future page benefits from. The computed-styles ordering bug, for instance, would have silently broken every default-styled element on every page with no author CSS, not just this one heading.

What's next

The actual reason I went digging that day was to add support for CSS attribute selectors things like [data-color-mode="dark"], which sites like GitHub use heavily to scope their dark/light theme variables. That part's now implemented and unit-tested against synthetic nodes, but not yet thrown at a real, CSS-heavy site.

Before I point Courage at something as complex as GitHub, I want to build up confidence on progressively bigger small sites first proving out attribute selectors and the rest of the CSS pipeline against real-world markup, not just fixtures I wrote myself. GitHub is still the long-term target; I'm just not skipping steps to get there.

If you're curious about the project, it's fully hand-built no cheerio, no jsdom, no rendering libraries and I'm documenting the whole thing day by day. More soon.

for github CLICK HERE!

Top comments (2)

Collapse
 
rushikes-dev profile image
Rushikesh Lade

Nice

Collapse
 
nitinkumaryadav1307 profile image
Nitin Kumar Yadav

thanks!