DEV Community

Profiterole
Profiterole

Posted on

I Built 9 Comparison Tools for Malaysian Personal Finance — Here's What I Learned

When you move to a new country — or just want to stop overpaying — you quickly realize how hard it is to compare financial products. Malaysian broadband plans, credit card rewards, home loan rates, car insurance premiums... the info is scattered across dozens of PDFs, outdated blog posts, and salesperson decks.

So I built comparison tools. Nine of them. All in vanilla JavaScript, no frameworks, deployed on GitHub Pages for free.

Here's what I learned.

Why Comparison Tools for Malaysia Specifically

Malaysia has a few quirks that make comparison particularly painful:

  1. Multiple telcos, similar plans — Maxis, Celcom, Digi, U Mobile, unifi all offer plans that look identical on the surface but differ in coverage, throttling, and contract terms.
  2. FD rates change monthly — Fixed deposit rates at Maybank, CIMB, Public Bank, RHB shift constantly, and aggregators are often stale.
  3. Credit card rewards are complex — A card might give 5% cashback on groceries but 0.2% on everything else. Depending on your spend mix, the "best" card is different for everyone.

The value is in filtering by your situation, not just listing data.

Technical Approach: Why Vanilla JS?

I deliberately chose no frameworks. Here's why:

  • Zero build step — Edit a file, push to GitHub, it's live in 2 minutes
  • No dependency rot — These tools need to work in 2 years without me touching them
  • Fast load on mobile networks — A 2G user in rural Sabah can still use the tool
  • Easy to reason about — Every comparison tool is one HTML file + one JS file

The tradeoff: more boilerplate. Worth it for the use case.

Data Structure Design

The core pattern I landed on after a few iterations:

const PLANS = [
  {
    id: "maxis-fiber-100",
    provider: "Maxis",
    name: "Fibre 100",
    speed_mbps: 100,
    price_myr: 99,
    contract_months: 24,
    features: ["WiFi 6 router", "No data cap"],
    tags: ["fibre", "home"]
  },
  // ...
];
Enter fullscreen mode Exit fullscreen mode

Key decisions:

  • Flat objects, not nested — Easier to filter with simple comparisons
  • Consistent units — Always price_myr, always speed_mbps. No ambiguity.
  • Tags array — Lets me do plan.tags.includes("fibre") filtering without complex logic
  • IDs for stability — If someone bookmarks a comparison, the ID stays stable

For the home loan calculator, I added a effective_rate computed field because the advertised rate and actual cost diverge significantly once you account for MRTA insurance and lock-in penalties.

Interactive Filtering and Sorting

The filter pattern that worked best:

function applyFilters() {
  const budget = parseInt(document.getElementById("max-price").value) || Infinity;
  const type = document.getElementById("plan-type").value; // "" means all

  const filtered = PLANS.filter(plan => {
    if (plan.price_myr > budget) return false;
    if (type && !plan.tags.includes(type)) return false;
    return true;
  });

  renderTable(filtered);
}

// Debounce for range sliders
let filterTimer;
document.getElementById("max-price").addEventListener("input", () => {
  clearTimeout(filterTimer);
  filterTimer = setTimeout(applyFilters, 150);
});
Enter fullscreen mode Exit fullscreen mode

The debounce on range sliders is critical — without it, dragging the slider rerenders the table 20 times per second and feels laggy even on modern hardware.

For sorting, I pass a comparator:

function sortBy(key, direction = "asc") {
  currentSort = { key, direction };
  const sorted = [...currentData].sort((a, b) => {
    const mult = direction === "asc" ? 1 : -1;
    return (a[key] - b[key]) * mult;
  });
  renderTable(sorted);
}
Enter fullscreen mode Exit fullscreen mode

Notice [...currentData] — always sort a copy, keep the original array intact for when filters change.

Dark Theme Without CSS Variables Hell

I went dark-first for two reasons: Malaysian users skew mobile and night-mode usage is high, and it's easier to start dark than retrofit it.

The approach that kept me sane:

:root {
  --bg: #0f172a;
  --surface: #1e293b;
  --border: #334155;
  --text: #e2e8f0;
  --text-muted: #94a3b8;
  --accent: #38bdf8;
  --accent-hover: #0ea5e9;
  --positive: #4ade80;
  --negative: #f87171;
}
Enter fullscreen mode Exit fullscreen mode

Every color goes through a CSS variable. When I add a new component, I never use hex directly. This paid off when I needed to tweak the color scheme — one block of variables, everything updates.

SEO for Location-Specific Tools

This was the biggest learning. Generic titles don't rank. These did:

  • "Best Broadband Plans Malaysia 2025 — Compare by Speed and Price"
  • "Malaysian Credit Card Cashback Calculator — Find Your Best Card"
  • "Home Loan Comparison Malaysia — BLR vs Fixed Rate"

Structured data also helped. For comparison tools, ItemList schema with each plan as a ListItem gets you rich results in Google. It's verbose to write but worth it:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ItemList",
  "name": "Malaysian Broadband Plans",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Maxis Fibre 100",
      "url": "https://hlteoh37.github.io/sorted-my/broadband/"
    }
  ]
}
</script>
Enter fullscreen mode Exit fullscreen mode

Also: last-updated dates matter hugely for financial tools. People won't trust an FD rate comparison with no date. I added a visible Data last updated: March 2025 to every tool.

Key Learnings After 9 Tools

1. Mobile-first is not optional.
Over 70% of traffic came from mobile. I tested every tool on a real Moto G phone before shipping. Tables need horizontal scroll, not shrinking text.

2. Less is more for inputs.
My first broadband tool had 8 filter inputs. Most users never touched 6 of them. I cut to 3 inputs (budget, speed, type) and engagement improved.

3. Data freshness is a feature.
One tool got negative comments because the data was 6 months stale. Now I have a reminder system. Stale comparison tools are worse than no tool — they erode trust.

4. Copy-paste friendly output helps.
Adding a "Copy comparison" button that formats the current filtered table as plain text got unexpectedly high usage. Malaysians share screenshots in WhatsApp — give them something shareable.

5. Explain the methodology.
For home loans, I added a collapsible "How we calculate this" section. Bounce rate on that page dropped by 30%.

The Result

Nine comparison tools covering broadband, credit cards, home loans, car insurance, mobile plans, fixed deposits, personal loans, e-wallets, and unit trusts — all live at sorted.my on GitHub Pages.

Total bundle size across all tools: under 50KB. No npm. No build pipeline. Just HTML, CSS, and vanilla JS that'll still work when Node.js is on version 40.

If you're building tools for a specific market, the formula is simple: find data that's hard to compare, make it easy to filter by user situation, keep it fast and mobile-friendly, and update it regularly. The tech is the easy part.

Top comments (0)