This foreword appeared here only after several articles had turned into a series. I went back to the beginning and left the future reader an explanation of what to expect.
And now, to the matter at hand…
The web used to be a different place. Not better, not worse—though there are those who would argue with me. But it was definitely freer, and there was more choice. Including the choice of which browser to use.
Much has been written about the first browser war and how IE destroyed Netscape. But the second browser war is somehow forgotten, even though its outcome is the reason we are now left with the all-devouring Chrome and its clones, Apple’s Safari, and a Firefox that is diligently burying itself with a measly three percent of the market. Everything else is basically a rounding error.
In 2012, we still had IE (who could have guessed that Microsoft would abandon it and switch to a competitor’s engine!) and Opera. An independent, one-of-a-kind browser that a great many people genuinely loved, developed by the Norwegian company Opera Software ASA.
Opera was both highly innovative and thoroughly niche. Its exact market share is open to argument, though nobody much disputes that on the desktop it was small. Its creators thought constantly about how to make the program better and easier to live with, and they did it without today’s corporate bullshit. Things like page tabs, speed dial, pop-up blocking, searching straight from the address bar, and much more were either invented or popularized by Opera. Across the former Soviet Union, Opera earned a special kind of love for how sparing it was with traffic. Caching everything in sight, switching off resources you didn’t need, rendering as the data arrived, and later Opera Mini and Turbo: you appreciated all of it if you were on pay-per-minute dial-up or per-megabyte DSL.
Opera ran on almost everything. Every operating system that mattered, feature phones, smartphones, game consoles, embedded devices… On many mobile phones of that era, it was the only browser capable of displaying graphics, executing scripts, and submitting forms.
And it was a fast, economical browser. The web had already begun to put on weight; pages were appearing that ran to—just imagine!—hundreds of kilobytes, even whole megabytes. Opera handled them with remarkable efficiency. Toward the end of its life cycle it was trading blows with Chrome on speed, holding its own admirably despite a staggering disparity in team resources.
With all these advantages, it seems surprising that Opera never conquered the world. In a fair and sensible world, Opera and Firefox would hold serious market share today, competing with Chrome and IE and making the web better, freer, and easier to live in. In the cursed world we built for ourselves, Opera couldn’t withstand the competition. The company decided to stop thrashing about: it switched to WebKit (and later to its fork, Blink) and sold off its intellectual property. The name “Opera” remains, but now it’s just another Chinese Chromium reskin—a decent one, they say. Former Opera employees are developing their own browser, Vivaldi—also a Chromium reskin, and also supposedly good.
But the Opera Presto source code, despite the pleas of the community, remained closed. I’ll talk about why later.
In November 2016, Opera Software ASA ceased to exist as a single entity, and in January 2017, someone “leaked” the ~2012 source code into the open. We don’t know who did it, but thank you.
As I write this, it is mid-2026. 14 years is several generations of change in standards, hardware, source code, principles, and approaches. Digging into these sources is like excavating Pompeii: under the ash of time lies a cross-section of how developers thought in that era, and of the technical choices that made the browser unique.
A few numbers to calibrate the scale. In the source tree:
.cpp/.h filesThere is some documentation right in the repository. It was never finished, and it links out to an internal wiki whose completeness we will never know. Instructions for actually building the thing are missing entirely, so that was the first problem I had to solve.
Now, watch closely: the codebase is unified across all platforms.
The browser that ran on Windows 7, macOS, QNX, and a Sony Ericsson feature phone was built from a single repository.
How was this achieved? With their own custom build system.
There is no separate “Windows version” or “QNX version” in the repository; the code you need is assembled on the fly. A build script walks every module, reading its metadata—plain text files describing which files to compile, which APIs the module exports and imports, and a good deal more. It matches that metadata against a given configuration and generates *_jumbo.cpp files, and those are what actually get compiled.
Modules do not reference each other by name. As noted above, a module declares its API imports and exports along with the conditions under which it needs them:
API_CACHE_URL_STREAM
URL_DataStream helper.
Import if: FEATURE_EXTERNAL_SSL
Conditions are boolean expressions over features and tweaks. The build script parses them into an AST, resolves them transitively (if A imports an API from B, and that API depends on feature C, then building A implicitly requires C), and turns them into preprocessor #defines. The result is a declarative dependency graph: modules describe what they need, and the build system calculates how to link them. For an engine with thousands of compile-time switches, this prevents an exponential explosion of makefiles.
So what are these jumbo files? They are a local implementation of a unity build. Normally every .cpp file is its own translation unit. The compiler runs on it alone, parses from scratch every header it includes, and emits a separate .o object file. With thousands of files and heavy headers, the redundant work is colossal: the same large header gets parsed thousands of times.
A unity build glues many .cpp files into a single translation unit, literally with #include. The compiler then runs once over the whole batch: shared headers are parsed once, the optimizer sees all the code at once and can inline across file boundaries, and the linker receives tens of object files instead of thousands. Compilation speeds up dramatically.
In Presto it looks like this. The operasetup.py generator emits *_jumbo.cpp files that simply include the sources:
// This file is automatically generated by operasetup.py
#include "core/pch_jumbo.h"
#include "modules/content_filter/content_filter.cpp"
#include "modules/content_filter/content_filter_module.cpp"
The price you pay is isolation. Inside a glued unit, file boundaries disappear: two static void helper() functions in different files, two symbols in anonymous namespaces, a file-scoped #define—anything that used to be local starts colliding once the files are glued. That leaves unity builds dependent on strict naming discipline and careful macro hygiene. Incremental builds suffer too: edit one file and the whole jumbo unit recompiles.
CMake gained a UNITY_BUILD option that does exactly this at the end of 2019. Presto had been running the technique as its normal mode since the 2000s.
The second layer is the builder itself, Flower, which lives in platforms/flower. It’s a Python 2 build system meant to replace make, and its documentation criticizes make on four counts:
make builds the dependency graph in advance, whereas in Presto, the list of source files is unknown until the setup phase;make consists of flat strings with no lists or dictionaries;make build is unreadable;make targets are not self-documenting.The name probably comes from the architecture. Flower is built on Python generators: a node is a unit of work, and a flow is a generator function describing how to build that node. yield pauses the flow until its dependencies are ready, which buys cooperative multitasking without threads.
The MSBuild path works the same way: the entry point is the operasetup project, which runs the very same hardcore scripts. It has its quirks, but it works.
And that’s not the end of it. Presto is configured entirely at compile time: no runtime flags, no feature flags in the modern sense. Four mechanisms do the work:
Features — large functional blocks switched on or off as a whole. They live in modules/hardcore/features/features.txt, all 416 of them. Each entry carries a name, the engineer responsible, a description, the resulting #defines, and its dependencies. The file generates the build profiles (profile_desktop.h, profile_minimal.h, and the rest), and the minimal embedded profile switches off about 70% of what the desktop profile switches on.
Each feature in the registry is a little questionnaire: description, owner, what it defines, what it depends on, and an “on/off” matrix across all five profiles. Reading features.txt is like flipping through a ship’s log: here is FEATURE_OUT_OF_MEMORY_POLLING — “enable if you have a lot of memory, but it’s still limited”; here is FEATURE_LIMITED_FOOTPRINT — “reduces code size at the cost of slower algorithms.”
Tweaks — constants (TWEAK_*), of which there are about 1,200 across the tree. Each tweak can have a different value for different profiles:
TWEAK_HC_FREE_MESSAGE_POOL_INITIAL_SIZE jl
Value : 64
Value for desktop : 512
Value for minimal : 16
Performance tuning, baked into the binary at build time.
Capabilities — macros like *_CAP_* (about 1,800 of them) that solve the problem of compatibility between module versions. Instead of “if module version ≥ X,” you write “if DPI_CAP_PLATFORM_EXECUTE is defined.” A module declares what it can do, and the consumer checks it via the preprocessor.
On top of all this lie product overrides in platforms/*/product/ — separate features.h, tweaks.h, system.h, config.h files for each platform.
The philosophy is clear: configure everything at compile time so that disabled code is simply absent from the binary, rather than checked at runtime. Zero cost for disabled features. For embedded devices in the 2000s, this was exactly the right call.
The downside is just as clear. First, the combinatorial explosion: 416 features multiplied by profiles yields a configuration matrix nobody can hold in their head, and a bug in one configuration may never surface in another. Second, code disappears en masse behind macros. The FEATURE_* names themselves rarely appear in the source. During build generation a feature expands into its derived defines, usually something ending in *_SUPPORT, and the whole engine is paved with them: nearly fourteen thousand #ifdef *_SUPPORT checks in the tree, and almost seventy thousand preprocessor conditions in all. So you change something and it “doesn’t work,” because in that particular configuration the preprocessor stripped the entire block out—and you find that out only when you run the tests. Leftover artifacts suggest the developers hit this too:
// Left-over from FEATURE_SCRIPTS_IN_XML, to be removed when no longer used.
#define SCRIPTS_IN_XML_HACK
The feature was killed off, but the hack it once enabled lived on.
The layers of this codebase could be studied forever. We can’t see the commit history, we don’t have the internal issue tracker’s archive, and internal documentation, if it ever existed, is out of reach. But the development culture of that era left artifacts of its own: bug tracking done in the comments, half-written documentation sitting beside the code, stray explanations, copyright headers. For all the engineering rigor, that was par for the course back then, and it tells us a great deal.
The oldest code dates to 1995: 2,733 files carry that copyright year, the engine’s Big Bang. Strictly speaking, the oldest line in the tree is from 1984—a GNU Bison skeleton hardcoded into the generated CSS parser. But the oldest living code is from 1990, and it comes with a story worth telling; we’ll get to it shortly.
Some of that documentation—which literally opens with “not much here yet”—lays out the development principles Opera held to for years. Two lines distill them:
It sounds silly even to mention it, but we do require C++ to be supported. Not every interesting platform has C++.
and
Some advanced C++ concepts are not required.
RTTI, STL, namespaces, exceptions: we don’t need them.
Again, remember where Presto ran. The engine’s feature registry lists five build profiles: desktop, smartphone, tv, mini, minimal. The exact same code was compiled into a desktop browser, a browser for feature phones, firmware for TVs and set-top boxes (there are still 71 files in the tree mentioning ATVEF—the interactive television standard), and the server-side renderer for Opera Mini. Looking at the code fossils, you can reconstruct the whole zoo: Symbian/EPOC (34 files), Qualcomm BREW (60), QNX and its Photon GUI, Windows CE. The compilers on those platforms were about what you’d expect: who needs the STL when it’s 2002 and your C++ is just “C with classes” built by a vendor’s phone toolchain.
The phrase “interesting platform” is a hyperlink, and it points at the Plan 9 from Bell Labs website. Opera’s engineers genuinely allowed for the possibility that somewhere out there sat an interesting platform with no C++ at all.
The “we don’t need the STL” decision had a consequence that shaped the entire look of the code: Opera’s engineers loved to write everything themselves (and what they didn’t write, they usually modified heavily). The tree has an stdlib module, their own implementation of the C standard library: op_strlen, op_snprintf, op_memcpy, and so on. They have their own allocator and memory manager (modules/memory), their own strings (OpString, built on a custom uni_char type—UTF-16, long before char16_t reached the standard), their own containers (OpVector, OpHashTable), and their own smart pointers (OpAutoPtr).
They wrote their own containers twice. In the otl module—the Opera Template Library—lies the second generation, and its documentation explains why: the old OpVector<Foo> stored Foo* pointers; the new OtlVector<Foo> stores values—”STL-like containers for core.” So a culture that proclaimed “we don’t need the STL” spent years arriving at its own STL, decided it had built the wrong one, and built another. Nearby is a third pass at a related topic: the opdata module with copy-on-write buffers. Reinvented wheels of different generations, all parked in the same garage: OTL took root sporadically in newer subsystems, OpData in scope/protobuf and network buffers, while ~90% of the code still lives on OpVector/OpString.
And now for the promised 1990 code. Formatted output—op_sprintf and friends. The file header preserves its pedigree: “This code is taken from the EMX run-time library… Copyright (c) 1990-1997 by Eberhard Mattes,” with an addendum: “Converted to joint ascii/unicode printf by lth@opera.com / November 2005.” Code written for the OS/2 GCC port runtime in 1990 was taught Unicode in 2005, and it still prints every single line in this browser today.
The random number generator was scavenged much the same way: a Mersenne Twister, Copyright 1997–2002 Matsumoto and Nishimura. All of it is meticulously documented in the feature registry, to the point that compiling the engine without the required flag runs you into a wall of #errors recounting where the code came from.
But these aren’t the only lengths Opera went to for the sake of universality. This code has its own OOM (Out-Of-Memory) discipline, exotic even for its time. The engine was written for devices where 16 megabytes was a luxury, and every single allocation in it is checked. No exceptions—instead, they have their own LEAVE/TRAP mechanism: setjmp/longjmp with a manual cleanup stack. The documentation still speaks of it as a trauma:
A typical example of something that was outside the envelope of change is the out-of-memory handling introduced in Opera 6 — it affected virtually every line of core code and was quite costly to implement.
“Affected virtually every line of core code.” That’s not a figure of speech: there are about 9,700 places in the tree using LEAVE/TRAP, and almost every function returns an OP_STATUS that cannot be silently ignored.
The mechanism itself lives in modules/util/excepts.h (the architect is listed as Petter Reinholdtsen), and its origin can be read in the names: the LEAVE macro internally calls User::Leave(i)—which is verbatim the EPOC OS API. The Norwegians modeled their error handling after a phone OS: TRAP is op_setjmp on top of a chain of CleanupItems, and classes like ANCHOR register objects for automatic cleanup when “jumping out.” These are hand-built exceptions: no compiler support, but guaranteed to work on any 1999 toolchain.
And this wasn’t a cargo cult: the memory module can run the engine on an artificially constrained heap (via dlmalloc) and deterministically drive it to OOM on a desktop—”a powerful combination allowing accurate testing of OOM situations,” as the docs say. They really did test it.
There was even an attempt to move to native C++ exceptions: the code carries defines for that path which were never used. My guess is that by 2012 there was no longer much point in supporting devices that weak, and newer compilers had stopped charging for exceptions in binary size. The developers reached for the idiomatic language feature and never finished, because rewriting three million lines riddled with OOM checks is hundreds of man-months of work.
Judging by the modelines, they wrote all this in emacs and vim. Console only. Hardcore only.
After everything so far, this will not surprise you: Opera’s testing system was homegrown too. Tests live in .ot (“Opera Test”) files inside the modules—a domain-specific language with setup blocks, data tables, and iterations. A test can be written in C++ or ECMAScript, and can carry inline HTML blocks.
A Pike script, modules/selftest/parser/parse_tests.pike, compiles .ot into C++, which is linked directly into the browser itself when the FEATURE_SELFTEST feature is enabled. Tests are not run as a separate binary; they are executed by a dispatcher inside the running browser—on desktop, this looks like Opera launching and opening some windows inside itself.
How many tests are there? 1,354 .ot files. The coverage is uneven but logical—the rendering core is covered best:
layout — 55 testssvg — 52dom (core + html) — approx. 80style — 32logdoc — 29url — 28forms, util, cache, pi — 23–26 eachLeft without tests: externalssl, webgl, applicationcache, obml_comm, xmlparser, updaters, and the third-party libraries. The team tested the critical subsystems seriously and left the periphery and the borrowed code alone.
For its time this was a respectable testing culture, though by today’s standards its limits are fundamental. The tests are integration tests in all but name: they run inside the assembled browser, with no unit-level isolation. Building them requires a working interpreter for Pike, a language even more exotic than Python 2. And there was no CI in the modern sense: tests were run locally inside the build, not on every cloud commit.
As a good historian once said: “We cannot get inside these people’s heads.” But the traces they left behind bring them into focus. Take the internal code names.
The engine itself is Presto, “quickly,” like the musical tempo. Opera’s JavaScript engines were a dynasty named after ancient scripts: Linear A and Linear B (Aegean scripts), then Futhark (runes), then Carakan (Javanese script).
Browser releases in the 9.x-10.x era were named after falcons (Merlin, Kestrel, Peregrine), and later came the fish (Barracuda, Swordfish, Wahoo — 11.x–12.00).
The graphics library is called VEGA, an acronym: “Library for VEctor Graphics with Anti-aliasing.” The JPEG decoder is called jaypeg, the PNG decoder is minpng (“focusing on minimal footprint”), and the password manager is wand. The widgets module is called gadgets, and its module.about explains why: “The module is called gadgets because, in the code, ‘widgets’ already has another meaning.” And the strange name logdoc for the core DOM tree is simply “logical document.”
A special mention goes to an Easter egg in the CSS parser. In the grammar, among other tokens, there is CSS_TOK_DOUBLE_RAINBOW_FUNC: Opera 11.60 introduced the -o-double-rainbow() gradient function. A Double Rainbow, hardcoded into the Bison grammar to allow developers “to sprinkle some vivid color into their web projects.” It was elegant mockery of the standards, and proof that the browser was made by real people with a sense of humor who enjoyed their work.
But the most “human” layer of this archaeological dig lies in the comments left in the code. 1,409 TODOs, 1,859 FIXMEs, 1,449 XXXs, ~850 links to the internal CORE-xxxxx bug tracker, and ~690 to the desktop DSK-xxxxx tracker. But statistics don’t convey tone. Here are my favorite finds:
// Warning: some cargo cult programming ahead.
if(!this) // I know, it is silly... :-( but if it prevent the crash, it's ok...
// Quirk crap. Try not to look.
// horrible hack, but it's probably not a good idea to font switch to ahem. ever.
// Rebuild the widget from the beginning. Yes, this sucks.
if ((p = strstr(head.longname, "?B?")) != NULL) // It's NOT Base64, it's QP damn it!
// Now we're screwed, there is no way out. Let's hope the error status...
const LayoutCoord o(0); // Avoid an insane amount of "LayoutCoord(0)" below.
// Sorry, this is ugly, but otherwise we crash
OP_ASSERT(!"What to do, what to do, with this strange content type");
It would be unfair to reduce this archaeology strictly to curiosities. Inside Presto lie things that still command professional respect today.
Carakan — the JavaScript engine — is a register-based virtual machine with JIT compilation to native code for four architectures: x86, x86-64, ARM (including Thumb), and MIPS; nearby in the utilities are assemblers for PowerPC and SuperH. Values are NaN-boxed (8 bytes on 32-bit platforms), and objects live on “hidden classes”—internal documentation calls this the Compact Object Model and boasts about reducing the overhead from 6–7 pointers per object down to one. The JIT performs type profiling directly in the bytecode and recompiles hot code as the picture becomes clearer. The regular expressions are also homegrown, and they have their own separate JIT for those same architectures. World-class work, done by one small team on their own infrastructure, without LLVM, in 2009.
VEGA — a vector rasterizer with a software backend and hardware ones: Direct3D, OpenGL, DirectFB. On top of it sit Canvas 2D, WebGL (with a fully custom parser and GLSL translator in modules/webgl—they wrote their own shader compiler), SVG with SMIL animation, and filters capable of rendering on the GPU.
HTML5 Parser — a complete implementation of the spec algorithm of that time: tokenizer, tree builder with all insertion modes, foster parenting, adoption agency, and a speculative pre-parser for resource prefetching. Internal name: Ragnarok. Presto wasn’t just fast; it was a highly modern engine.
Text. A custom OpenType shaper with support for Arabic and Indic scripts (without HarfBuzz—it barely existed then), a custom implementation of the Unicode Bidirectional Algorithm, Unicode 6.1 tables, a charset detector, and three dozen charset decoders. All their own.
Network. Again, all homegrown: HTTP/1.1 with aggressive pipelining (the code houses flags like tested_http_1_1_pipelinablity and a pipeline_problem_count counter: the engine tested every server for compatibility and backed off if issues arose), SPDY versions 2 and 3, WebSockets complying with the final RFC 6455, and CORS with beautifully implemented preflight. And FTP. And a Gopher stub—obviously.
And the CSS pioneering for which web developers loved (and sometimes hated) Opera: -o-tab-size, -o-object-fit (object-fit was invented at Opera), @viewport (a CSS alternative to the viewport meta tag), and GCPM floats for print layouts. In the properties table, two generations of flexbox peacefully coexist within a single file: the final display: flex from 2012 and the ancient -webkit-box from 2009.
If you stack all these layers together, a portrait of the team emerges from the excavation. People who seriously intended to run on every platform in the Universe—and therefore wrote everything for themselves: libc, STL, an allocator, a font shaper, a shader compiler, four JIT backends. People with almost military discipline regarding metadata—who also left lively, funny comments. People who rehearsed out-of-memory scenarios on feature phones—and hid a double rainbow in the CSS grammar. This was a vast, intelligent, distinctive engineering world, populated by people who loved what they did.
After Opera ASA decided to switch to WebKit, a lie was born, rivaling the infamous “640K ought to be enough for anybody.” Users—most of them people with an IT background—were told (and I quote): “all these changes will happen under the hood for regular users”.
People took this to mean “instead of Presto there will be WebKit, instead of Carakan there will be V8, but all the features I chose this browser for will remain.” And there were plenty of those features; some have still never been implemented anywhere else.
Thirteen-year-old spoiler alert: we were tricked. Bam-boo-zled. Played like a fiddle.
Before long, “changes under the hood” had become a bitter meme in the community, and Opera had become just another Chrome reskin. You know this story; I won’t dwell on it.
Could they have done it exactly as announced? No, and the reason goes back to that same principle of keeping everything in-house. Opera’s desktop interface was called Quick, and every widget in it was custom-drawn. Opera didn’t use the operating system’s buttons, lists, or input fields; it drew them all itself. OpButton, OpTreeView, OpEdit, OpToolbar, OpWorkspace, DesktopWindow, BrowserDesktopWindow — these were all rendered by the widget engine (modules/widgets) on top of the same VisualDevice used for web pages, while the visual appearance was pulled from the skinning system (modules/skin and OpSkinManager).
That is why Opera 12 looked identical on Windows, Mac, and Linux, and why it was so extravagantly customizable: the famous Opera skins could change the entire interior beyond recognition. Every pixel of the interface was drawn by Opera itself, and it asked very little of the OS. The platform layer (pi plus platforms/unix, platforms/windows, platforms/mac) only provided:
Everything else—tabs, panels, the address bar, sidebars, settings—was drawn by Quick. Roughly speaking, Opera’s interface was about 90% platform-independent, with platforms/ serving as a thin shim.
The code does show an intention to put an interface between the engine and the UI: the OpWindowCommander class offers a clean, narrow embedding API. But the boundary was routinely bypassed—Quick modules dug straight into the engine’s guts, helping themselves to its internal classes and objects. WindowCommander ended up less an embedding API than an event notification layer.
And it goes further. The UI and the engine shared everything: the same build system, one binary, the same error-handling idioms (OpStatus, LEAVE/TRAP), the same OpString, a single global g_opera, the same pi layer. Quick wasn’t an application embedding a web engine through a stable interface. Quick was compiled into the engine—one monolithic executable in which the UI called the engine directly, like a neighboring module.
So in technical terms, “keep the interface and swap the engine” meant “rewrite the entire interface from scratch”: rip out every direct call to Window, FramesDocument, and VisualDevice, replace the document model, rewrite the bindings for a foreign engine, and overhaul the build system. You cannot pull off a change like that “under the hood.”
But why not just open-source the code that nobody needed anymore? Who would that have hurt?
Vadim Makeev, Opera’s evangelist at the time (and the author of that infamous “under the hood” blunder), cited three reasons:
FEATURE_3P_ITYPE_ENGINE) and the Matrix SSL library (FEATURE_3P_MATRIX_SSL). On top of that lay non-distributable data: a root certificate store with trusted anchors, search engine data, and ~197 MB of translation databases tied to Opera’s internal infrastructure.Opera ASA could have open-sourced the desktop versions of the browser at any moment, but they chose not to.
As you have gathered, Opera’s engineers preferred to do everything themselves whenever they could. When that wasn’t an option, third-party code came in with no external dependencies and often with modifications of their own. There is a fair amount of it: sqlite, zlib, libfreetype, and more. The largest and most interesting borrowing is a heavily modified fork of OpenSSL. The module is called libopeay, which breaks down as lib + op(era) + eay — the initials of Eric A. Young, author of SSLeay, the library OpenSSL grew out of in 1998. Its artifacts show the code was ported over several times, from the late SSLeay era (versions 0.8.x) up to the then-current OpenSSL 1.0.0g. They didn’t take the upstream source wholesale. A dedicated script carved out everything unnecessary: apps, demos, test, documentation, platform wrappers for VMS and OS/2. Then another script wrapped each remaining .c file in its own opera_*.cpp, because thousands of OpenSSL C files would never have survived the jumbo build with its single translation units and name collisions. After that came a great deal of work by hand. So, even “taking something off the shelf” at Opera meant running someone else’s code through their own meat grinder.
Out of all of OpenSSL, though, Opera used only the cryptography: bignum math, RSA, DH, ciphers, hashes, X.509, ASN.1. The TLS protocol itself—handshakes, records, sessions—was written from scratch in-house. OpenSSL ships its own SSL/TLS implementation (the ssl/ directory, files like s3_clnt.c, t1_enc.c), and those files are in the tree, wrapped in opera_s3_clnt.cpp—but they carry the #[external-ssl-only] tag. On desktop they were never compiled at all. The desktop browser encrypted its traffic with a stack of its own: the libssl module.
Why write your own TLS? For the same reason you write your own STL and libc: control and portability. An in-house implementation could obey Opera’s OOM discipline, its asynchronous model, its certificate manager and dialogs. The boundary is declarative: libssl asks libopeay for an API of crypto primitives, and nothing more. libssl also carries extensions that OpenSSL lacked, or that Opera bolted on its own way:
asynch_rsa_gen is not a function but a state machine: RSA_KEYGEN_INITIALISING → GENERATING_P → DONE_GENERATING_P → .... Key generation is split into steps, between which the engine processes messages.cert_ex_data file (2007–2008) attaches Opera’s Extended Validation data to X.509 structures via the CRYPTO_EX_DATA mechanism. Opera extended someone else’s structure without touching its code.elgamal) — which had been ripped out of OpenSSL by that time, but Opera needed it, so they brought it back.The engine implements protobuf natively, without the Google Protocol Buffers runtime. That powers the remote debugging, from the days when you could debug a page on your phone straight from your desktop. The debugger itself, Opera Dragonfly, was architecturally decoupled from the engine and ran on top of it as a web application, so new versions could be fetched from Opera’s servers without updating the browser. Its source code was open-sourced.
Vector graphics are handled in modules/libvega, and there are three backends for it: a software rasterizer (vegabackend_sw), 2D hardware acceleration (vegabackend_hw2d), and a full 3D pipeline on the GPU (vegabackend_hw3d)—the latter being necessary for CSS transforms and effects. Paths (VEGAPath) are rasterized using a sweep-line algorithm; gradients, patterns, and blend modes are supported.
Beneath that lies modules/libgogi, a low-level graphics layer called the “Multiplatform Desktop Environment” (MDE): a region-based invalidation system that tracks the screen rectangles needing a redraw and merges the overlapping ones. A classic solution from the pre-GPU era, when the goal was to push as few pixels to the framebuffer as possible.
The engine’s core is single-threaded. There is one message loop, and everything runs through it: HTML parsing, the CSS cascade, layout, JavaScript execution, rendering. The DOM tree and the box tree are not protected by mutexes or atomics—and they don’t need to be, because they are always accessed by exactly one thread. In several places, a comment reminds you: THIS IS NOT THREAD-SAFE.
A curious detail: the platform layer has a PI_CAP_SYNCOBJECT_REMOVED macro—it seems Presto once had thread synchronization objects, and they removed them.
Presto does have a framework for isolating processes that communicate by message—not multithreading but multiprocessing, much like Chrome. Or rather, it would have had one: the work was never finished. The approach is impressive all the same.
The multiprocessing component module provides entities like OpComponent/OpComponentManager/OpComponentPlatform/OpTypedMessage/OpChannel. The model assumes one OpComponentManager per process; components communicate exclusively via OpTypedMessage over channels; an address is a triplet: “manager.component.channel”. The communication format is serialized protobuf (component.proto: source/destination Address{componentManager, component, channel}, type, bytes data). The transports are exactly what you’d expect: posix_ipc on *nix, and shared memory + events on Windows.
The framework was intended to be broad (“thread or process,” multiple worker types), but in the actual 12.15 codebase, the threading branch consists of stubs.
The one place the mechanism saw partial use was isolating NPAPI plugins (Flash Player, Java applets, and the like): if a plugin crashed, it didn’t drag the engine monolith down with it. When x64 support arrived, the same mechanism came in handy for running 32-bit plugins through a wrapper.
Pike—a programming language you’ve never heard of—was widely used at Opera. Today, and even back in 2012, that looks strange; Python seems the far better choice. Why keep Pike around just to build tests when Python is already building the code? But Opera’s engineers never did anything without a reason, so I got curious and dug in.
The first clue was in the copyrights: parts of the test compiler bear Copyright (C) 2002. This is the Opera 6/7 era. The second clue is in the feature registry: FEATURE_3P_PIKE_UNIVERSE — “Pike… used in the Opera Mini servers.” The third clue: various scripts—internationalization table generators, Doxygen runners, some formatters. Pike was at Opera before Python was, and the team knew it well.
Why this language, and not, say, Perl? The .ot test format isn’t just data: embedded inside the tests are blocks of C++ and JavaScript. To compile an .ot file into C++, the parser needs to know how to tokenize C-family code. And here Pike had the advantage: its standard library already shipped a C/Pike tokenizer, the Parser.Pike module, which handled braces, strings, comments, and the C++ preprocessor out of the box.
After that the golden rule applied: if it ain’t broke, don’t fix it. Flower was easier to write in Python, but test compilation stayed on the exotic interpreter.
Perl is still used, though; the localization build runs on it.
Two legendary Presto features have never been properly replicated anywhere else: mouse gestures and fully customizable keyboard controls. From an engineering standpoint they are beautifully done, with both living in a single namespace of virtual keys, OpKey::Code, described in modules/hardcore/module.keys:
OP_KEY_GESTURE_UP GestureUp gesture (module.keys:247, + Down/Left/Right and diagonals)
OP_KEY_FLIP_BACK FlipBack flip (module.keys:258, + FlipForward)
The gesture recognizer (MouseGesture::CalculateMouseGesture) turns a stroke into a direction and returns a specific OP_KEY_GESTURE_* code—and then feeds it into the exact same dispatcher as the keyboard: InvokeKeyPressed(gesture, ...). Rocker gestures (holding the left mouse button and clicking the right) synthesize OP_KEY_FLIP_BACK/FORWARD. To the engine, then, a “down-left gesture” and “Ctrl+Z” are events of exactly the same kind, differing only in the ActionMethod { METHOD_MOUSE, METHOD_KEYBOARD, ... } field.
Every module defines a list of its actions in module.actions, and simple .ini configs describe the bindings of methods to actions depending on the context:
GestureLeft, GestureDown = Rewind, 0
GestureRight, GestureUp = Fast forward, 0
Platform Mac, SwipeLeft = Back
...
z ctrl = Undo
1 ctrl = Go to speed dial, 1
Feature ExtendedShortcuts, 1 = Switch to previous page
Users could remap absolutely everything by editing these .ini files.
That makes it clear why these features “didn’t make the cut” during the migration. This is an entire core input subsystem: its own virtual key namespace, a recognizer built as an extension of the keyboard, a compile-time action table, and text configuration layered on top. Nothing like it exists in the Chromium model, and replicating “a gesture is a key, and bindings are editable text that inherits by context” would have meant dragging in a wholly foreign input architecture. It was easier not to bother—and Opera on Blink launched without gestures, only to hack them in (partially, via extensions) much later and in a completely different way.
And what did all this carefully planned architecture buy Presto? Portability, first and foremost.
Let’s look back at the internal documentation:
…there is a summary of the estimates time it takes to port Opera to a new platform, based on the ports to Kyocera and PowerTV. The figures are probably guesstimates as much as estimates, but adding them up it comes out to between 110 and 170 man-days for a port of the full browser, not including customer extras.
An engineering team could port the browser to a new platform in about a month. Not bad.
And finally, a word on how Opera survived financially: affiliate links. They are everywhere: hardcoded bookmarks, Speed Dial links, default search engines. Every click brought in a penny, and pennies make dollars.
That revenue stream had to be protected—not from the user, but from some rogue extension that might hijack the links. So they built Search Protection, a mechanism that defended these settings against modification. Search engine files were RSA-signed, the chosen default was guarded by a checksum, and on a mismatch the browser silently rolled the setting back to the signed copy.
There is plenty more in the code, fascinating and unhinged in equal measure, that I want to talk about. There’s material for three more chapters like this one, but I restrained myself.
Building the code on Linux turned out to be surprisingly easy, outdated toolchain and all. The only exotic requirements are Python 2.7 for Flower and Pike for the tests (Perl doesn’t count; any current version from any current distro works fine), and both are easiest to shove into a Docker image once and forget about. The engine itself built on a modern GCC 14 after one minor fix and a dozen compiler flags:
-fpermissive, -Wno-narrowing, -Wno-deprecated, -Wno-register, -Wno-class-memaccess,
-Wno-deprecated-declarations, -Wno-misleading-indentation, -Wno-address,
-Wno-stringop-overflow, -Wno-stringop-truncation, -Wno-array-bounds, -Wno-restrict,
-Wno-format-overflow, -Wno-format-truncation, -Wno-nonnull, -Wno-dangling-pointer,
-Wno-write-strings
Building in VS 2026 took more fixing, both in the language (declarations modern MSVC finds “incomprehensible,” plus other small things) and in the tooling. libvpx, for instance, is built through an integration with the yasm assembler. I couldn’t get vsyasm to run, but a Python wrapper took ten minutes to write. I also had to add paths to 7zip (needed for packing skins) and to Perl.
And that was enough. The builds compiled and ran, the tests executed and passed—except for a few that were apparently tied to internal mock servers. Almost boringly so.
And it builds fast: on an Intel Core Ultra 9 275HX a full single-threaded build takes 162 seconds, bottoming out at ~103 seconds when heavily parallelized (up to 24 threads). That modest 1.5x scaling is the nature of jumbo builds in a nutshell: the bottleneck moves from CPU cores to memory bandwidth.
Did I find bugs? Of course. The most interesting one: the Windows x64 build crashed on every page, while the Linux x64 browser ran flawlessly. The dig led to the JIT: the trampoline transitioning from bytecode to native code didn’t account for the Win64 calling convention—which uses different registers for argument passing and requires a mandatory 32-byte shadow space that the Unix convention knows nothing about. Add to that a scattering of classic 64-bit porting blunders: long where a pointer is needed, truncating HANDLE to int, and a special gem—modern MSVC optimized away the entire table of JIT instruction handlers because, technically, nobody was “referencing” it.
Opera did ship x64 builds from version 12 onward, Windows included, and naturally those official builds didn’t have this bug.
Fourteen years for a browser is several technological eras. Twenty or even fifteen years ago, a new browser version might ship every few months, maybe every six. Then, right around the early 2010s, the pace went exponential. The web Opera was built for no longer exists.
The first thing we run into is security. Opera 12.15 optionally supports TLS up to version 1.2, and while that is technically still enough to establish a secure connection with most websites, Opera can no longer verify a single certificate and will throw security warnings at every step.
As a temporary fix, you can disable this in the code, forcing Opera to trust all sites. As a permanent fix, you need to update the root certificates and implement TLS 1.3.
Opera’s SSL stack, as you’ll recall, is in-house, albeit built on OpenSSL. Its ceiling is TLS 1.2 (the code for it is marked TLS 1.2 (Jan 21, 2008: Not yet debugged)) with RSA/DHE key exchange and CBC/RC4/3DES ciphers. Bolting TLS 1.3 with ECDHE and AEAD onto that is technically possible, but it means recreating ten years of cryptographic evolution by hand while an open, battle-tested OpenSSL 3 sits right there. Handing the TLS protocol layer to the library looks like a quick win—and the infrastructure for it already exists, since Matrix SSL was used in exactly this scenario.
In Presto’s architecture the TLS layer is ProtocolComm, one link in the connection processing chain. Next to the native SSLConnection I slotted in a new OpenSSL_Record_Layer, inside which OpenSSL works through a memory BIO: the engine never hands the library a socket, it pumps bytes between the library’s buffers and its own asynchronous transport. Consumers up and down the stack never noticed the switch. The transplant took almost painlessly, except that I had to re-teach Opera how to read certificate and cipher information.
Certificate verification, however, remained on Opera’s side: its store, its trust policy, its dialogs. OpenSSL handles the handshake, but the “trust or not” decision is made by the engine—using an updated set of Mozilla root certificates plus the OS system store (a minor concurrent tweak). This preserved the entire native security UI, the certificate manager, and the user experience.
It wasn’t all smooth, though. Visiting a site with a self-signed certificate would “freeze” the browser. TLS wasn’t to blame: the warning dialog was being drawn invisibly, as an overlay on top of a page that didn’t exist yet, since the connection was paused mid-handshake. A classic UI bug: “the window is there, but nobody sees it.” It had been sitting in Quick UI all along, because the native stack simply never got as far as that dialog.
And then came the best part. Once the protocol migrated to OpenSSL 3, the entire frozen copy of OpenSSL 1.0.0g was no longer needed. The commit deleting it was 2,119 files and nearly 470,000 deleted lines of code. Out of libopeay, 127 files remained: headers and Opera’s addon extensions—but even those weren’t thrown away; they were ported to OpenSSL 3: asynchronous key generation is now asynch_rsa_gen_ossl3.cpp sitting on top of the EVP API, and EV data and purposes moved to the new X.509. The stuff Opera bolted on top of someone else’s library was carefully preserved.
As a final touch, a build switch was added, and now OpenSSL can be compiled statically or loaded from system libraries.
Aside from the massive OpenSSL fork, Presto is frozen with embedded third-party libraries circa 2005–2012: zlib, SQLite, FreeType, lcms, libwebp, Hunspell, and the list goes on. Updating them isn’t strictly necessary, but we’re not here to use this browser; we’re here to look under the hood.
Blindly updating the code doesn’t always work, though for some libraries it was routine, or needed only minor fixes. FreeType went from 2.4.8 to 2.13.3 without regressions; everything matched, right down to two “crashing” tests crashing the same way on old and new. libwebp went from 0.2.0 to 1.6.0, and there I had to regenerate the test assets, because the new renderer simply drew better. Same story with lcms, where the defaults for the alpha channel had changed.
Some third-party libraries contained specific patches. For instance, in zlib, Opera grafted a notion of data “classes” onto deflate—their defense against the CRIME attack for compressing SPDY headers (cookies are compressed separately from the rest so that the size of the compressed output doesn’t leak the secret). When updating to 1.3.1, this patch had to be ported to modern deflate and covered with tests under sanitizers. A bonus discovery: the old fork had a latent configuration bug causing all compression levels to degrade to the weakest one—it’s highly likely nobody at Opera ever knew their deflate-9 was actually deflate-1.
SQLite was modified too: Opera turned the progress handler into a cooperative scheduler, so the query virtual machine could pause mid-query and resume the exact same query from the same opcode (stock SQLite kills a query when it’s interrupted). Porting that patch from VDBE 3.7.9 to 3.53.3 was an adventure of its own, complete with an off-by-one at the resumption point. Testing also turned up that modern SQLite had raised the page size from 1K to 4K, which promptly blew up the Web SQL quotas hardcoded for kilobyte pages.
At the time of writing, only Hunspell remains untouched: the library changed both its operating principles and its API, and its integration is so invasive (a great rarity in Opera’s code, though justified here) that I haven’t yet mustered the courage.
Alongside all this, the toolchain was being dragged into the modern era. The easiest part was porting Flower from Python 2.7 to 3.13: mechanical work, with 2to3 doing most of the heavy lifting.
Replacing Pike with that same Python took considerably more work, and I judged it essential: the language is too exotic, and there is no reason to keep it around today. I ran the old Pike in a Docker container and pumped the entire test corpus through it to generate baselines. The Python port was then compared against those baselines byte for byte, right down to reproducing the original’s bugs—Pike stripped trailing spaces in one template and interned dead strings, and the port does the same, because those bytes end up in compressed string tables. The result: 98.3% of the corpus files produce byte-identical tables, all 1,354 files compile, and as modules came online the tests started going green by the thousands: util — 530/530, DOM — 2474/2479, standard library, unicode, formats…
The failing tests proved to be the most interesting.
url.headerreduction test has been broken since the Opera days: the test tables capture global char* pointers in the constructor before they are initialized. The port reproduced Pike’s behavior bit-for-bit—proving that the test never passed for them either.<svg> in Presto never participated in inline layout—adjacent SVGs stack vertically in defiance of CSS. Apparently, this is another test that was never actually executed.Tests are a boring topic until they start telling stories like this.
Next it was time to touch the C++. Opera’s code was written to C++98 / C++03 and carried a mountain of reinvented wheels, including macros that simulated features which later made it into the standard. Building it under a modern GCC threw over six thousand warnings. Ripping out libopeay and updating the internal forks quieted things down a little.
Then came the painstaking part: the language level went to C++17 without -fpermissive, then to C++20. Zero warnings on GCC and MSVC at /W4 and /permissive-; register and throw() are gone. The code is nearly “C++23-clean”—in a test build against the new standard only a handful of files break, and all of them for one elegant reason I can’t help but share.
OpString has a “stealing” copy constructor: it takes a non-const reference and takes ownership of the source’s buffer. Move semantics written in 2003, eight years before C++11. The problem is that C++23 (P2266) got much more aggressive about returning local variables as rvalues, and the ancient constructor, which demands an lvalue, stopped binding.
And here is the central philosophical question facing any restorer: should I rewrite all of this in “normal” modern C++ — std::string, std::unique_ptr, exceptions? Technically I could. I decided no. That original manifesto—”RTTI, STL, namespaces, exceptions: we don’t need them”—still stands. Not out of nostalgia, but for the sake of identity: Presto with std::vector inside it is no longer Presto. The fabric of this code—the OOM discipline, the custom containers, the custom libc—is exactly what I want to preserve.
The practical compromise I settled on: use the language, not the standard library. The reinvented wheels are modernized in place: OpAutoPtr became move-only (copy = delete) and got its own op_move—three lines, identical to std::move, without pulling in a single standard header. A naive attempt to swap OpAutoPtr for std::unique_ptr crashed the browser on startup instantly, which illustrates the cost of such replacements perfectly: calling reset(same_pointer) on Opera’s pointer is a no-op, while on the standard one it frees the object and leaves you a dangling pointer.
Opera had more dead, experimental, and just plain crazy features than you could shake a stick at. They clearly loved experimenting in Oslo—count them off:
I’m sure I’ve forgotten something, but you get the point. A good chunk of this simply won’t work today. Some of it can be restored; some isn’t worth reviving.
But these are pieces of history. Throwing them away feels wrong, and fortunately Opera was built so that a non-working feature can simply be hidden behind a feature flag. The useless code stays where it is, mothballed but ready.
Except for the macOS code. That was a port to Mac OS X (Cocoa with remnants of Carbon and rudiments of the PowerPC era), which is unbuildable in modern Xcode—I ripped it out without a twinge of conscience.
Current status: Presto builds on Debian 13/GCC 14 and Visual Studio 2026, runs on Linux x64, Windows x86, and Windows x64, opens modern HTTPS sites via OpenSSL 3 with TLS 1.3, has survived eight dependency updates, and keeps thousands of its own tests green. For a project that started with “will this thing even compile,” that’s not bad.
But using it on the modern web is out of the question—at best you get broken layouts and content that doesn’t work. A good share of resources won’t render at all. Presto is frozen at 2013: ES5.1 with no Promises or arrow functions, CSS with no grid or custom properties, HTTP without version 2.
I don’t want to jump the gun on whether an old Opera can be taught new tricks. We’ll wait and see.
I can’t publish the compiled binaries or the source (except perhaps as patches, though I doubt anyone would want to bother with those). I’m open to suggestions, and I’m thinking about what could be done to actually release the project.
Also, there will be no links here to a donation page. The point of all this isn’t the money.
The point is that Opera was a browser built with love and understanding. You can see it in every line, from the “we don’t need the STL” manifesto to the double rainbow in the CSS grammar. Refusing to publish the source felt like an injustice back then, and judging by the fact that someone eventually leaked it anyway, people inside the company felt the same. Some of them surely hoped a deep dive like this would happen one day.
Well, now it has.