Last week, I went from WakaTime global rank #135 to #2. Not by building some flashy new framework or jumping on the latest AI hype train. I rebuilt Lens Browser's ad blocking system from scratch because the old one sucked. ๐ค
Let me show you what 78 hours and 49 minutes of focused development looks like. โก
๐ฅ The Problem
Lens Browser is a privacy-first mobile browser. Think "open and go"โno accounts, no sync, no tracking. But here's the thing: if you claim to be privacy-first and your ad blocker barely works, you've failed. ๐ฌ
My old implementation:
- โ ~30-40% blocking rate
- โ False positives breaking legitimate content
- โ Noticeable performance impact
- โ No user control
That's not acceptable. โ
๐๏ธ The New Architecture
Three-Layer Defense System ๐ก๏ธ
// Pseudo-code showing the decision flow
fun shouldBlockRequest(url: String): Boolean {
// Layer 1: Check whitelist
if (whitelistManager.isWhitelisted(url)) return false
// Layer 2: Check manual blocks
if (blockedDomainManager.isBlocked(url)) return true
// Layer 3: Check against 80k rules
if (adBlockerEngine.matches(url)) return true
// Layer 4: JS observer handles DOM-level blocking
return false
}
Layer 1: Connection Interceptor ๐
Catches requests before they hit the WebView. This is where the magic happens:
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url?.toString() ?: return null
if (shouldBlockRequest(url)) {
return WebResourceResponse(
"text/plain",
"utf-8",
ByteArrayInputStream("".toByteArray())
)
}
return super.shouldInterceptRequest(view, request)
}
Layer 2: JavaScript Sanitizer ๐งน
Even if something slips through (dynamic content, inline scripts), the JS layer catches it:
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (isAdElement(node)) {
window.LensBridge.shouldBlock(node.id, result => {
if (result) node.remove();
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
๐ The 80,000 Rule Problem
You can't iterate through 80,000 rules for every request. That's O(n) per requestโdeath by a thousand cuts. ๐
My Solution: โจ
class AdBlockerEngine {
private val domainTrie = Trie()
private val urlPatternMap = HashMap<String, Pattern>()
private val bloomFilter = BloomFilter(expectedElements = 80000)
fun matches(url: String): Boolean {
// Fast negative check
if (!bloomFilter.mightContain(url)) return false
// Trie-based domain lookup: O(m) where m = domain length
val domain = extractDomain(url)
if (domainTrie.search(domain)) return true
// Pattern matching for complex rules
return urlPatternMap.values.any { it.matches(url) }
}
}
Performance Results: ๐
- Average request processing: <5ms โก
- Memory footprint: ~30MB for 80k rules ๐พ
- False positive rate: <1% โ
๐ Real-Time Statistics
Users want to see what's being blocked. I built a live counter: ๐
data class BlockedContent(
val domain: String,
val url: String,
val timestamp: Long,
val type: BlockType
)
object BlockStatistics {
private val blockedItems = mutableListOf<BlockedContent>()
fun addBlocked(item: BlockedContent) {
blockedItems.add(item)
notifyObservers()
}
fun getStats(): Stats {
return Stats(
totalBlocked = blockedItems.size,
uniqueDomains = blockedItems.map { it.domain }.distinct().size,
blocksByType = blockedItems.groupBy { it.type }
)
}
}
Users can now see:
- Total blocked count (updates in real-time)
- List of blocked domains
- Full URLs that were blocked
- When they were blocked
๐ฏ Key Features Shipped
1. Privacy Mode ๐
Hides User-Agent, screen resolution, timezone, and other fingerprinting vectors:
webView.settings.apply {
userAgentString = "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36"
// More obfuscation here
}
2. Trusted Domain Management โ
Whitelist system because not everything is an ad:
class WhitelistManager {
fun addDomain(domain: String) {
whitelist.add(domain.lowercase())
persistToStorage()
}
fun isWhitelisted(url: String): Boolean {
return whitelist.any { url.contains(it) }
}
}
3. Security Modal Before Load ๐จ
Old way: Load page โ detect threat โ warn user (too late) โ
New way: Detect threat โ show modal โ user decides โ
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
if (threatDetector.isDangerous(url)) {
showSecurityModal(url, threatLevel)
view?.stopLoading()
}
}
4. Blocklist Updates ๐
Users can refresh the 80k rules without reinstalling:
suspend fun refreshBlocklist() {
val newRules = api.fetchLatestRules()
adBlockerEngine.updateRules(newRules)
notifyUser("Blocklist updated!")
}
๐งช Testing & Results
SuperAdBlockTest.com Results: ๐
- Before: ~35% blocked โ
- After: 65%+ blocked โ
Real-World Testing: ๐
- CNN.com: 47 trackers blocked ๐ซ
- Reddit.com: 23 ad domains blocked ๐ซ
- Random blog: 15 tracking scripts blocked ๐ซ
Performance: โก
- Page load time: -15% (faster with ads blocked) ๐
- Memory usage: +8% (acceptable for 80k rules) ๐พ
- Battery impact: Negligible ๐
๐ก Lessons Learned
1. Premature Optimization Is Real ๐ฏ
I spent 2 days building a "perfect" rule matching algorithm. Scrapped it. The current one is 95% as good and shipped in 4 hours.
2. Test on Real Sites ๐
SuperAdBlockTest is great for benchmarking, but real sites (news, e-commerce, social media) show where your blocker actually fails.
3. Users Want Control ๐๏ธ
The #1 feature request: "Let me whitelist this site." Privacy users want agency, not just defaults.
4. Performance Matters More Than Features โก
I could add 50 more features, but if the browser is janky, nobody will use it. Every millisecond counts.
๐ฎ What's Next
This update proves you can build genuinely private software without VC money or user tracking. But I'm not done: ๐ช
- [ ] Custom blocklist imports
- [ ] Enhanced fingerprint resistance
- [ ] Per-site settings
- [ ] Optional tab management
- [ ] Sync (optional, encrypted, self-hosted)
๐ฒ Try It
Lens Browser is free and will never have ads or tracking. ๐
๐ฑ Download on Google Play
๐งช Test it on SuperAdBlockTest.com
๐ The Stats
- WakaTime rank: #135 โ #2 globally ๐ฅ
- Hours coded: 78h 49m โฑ๏ธ
- Daily average: 11h 15m ๐ช
- Lines changed: Several thousand ๐ป
- Coffee consumed: Yes โ
๐จโ๐ป For Other Developers
If you're building privacy tools:
- Start with threat models ๐ฏ: What attacks are you preventing?
- Measure everything ๐: Can't improve what you don't measure
- Ship imperfect code ๐: Iterate fast, fail fast
- Test with real users ๐งช: They'll break your assumptions immediately
Building privacy tech is hard. Building usable privacy tech is harder. But it's worth it. ๐ช
What ad blocker do you use? Drop a comment. ๐ฌ๐
P.S. If you're working on similar problems, let's chat. Privacy should be accessible to everyone. ๐โจ
Top comments (0)