Every long-lived Salesforce org accumulates a layer of Classic-era tech debt: Visualforce pages, Process Builders stacked three deep, and JavaScript buttons nobody wants to touch. Migrating to Lightning is rarely the hard part — doing it without a risky big-bang cutover is. Across multiple migrations I've settled on a playbook that ships value incrementally and, in one case, improved system performance by 40%.
Audit before you touch anything
Start with an inventory, not a rewrite. You want to know what's actually used before you spend a sprint modernizing a page three people open a year. I look at:
- Visualforce pages ranked by traffic and business criticality.
- Process Builders & Workflow Rules — candidates to consolidate into Flow.
- JavaScript buttons — each needs a Lightning-safe replacement (Quick Actions, LWC).
- Hard dependencies — managed packages or integrations that assume Classic.
Migrate by value, not by alphabet. The 20% of components that carry 80% of the usage are where Lightning pays off first.
Visualforce → LWC, one component at a time
The biggest wins come from replacing heavy Visualforce with focused Lightning Web Components. LWC renders on the client, leans on the platform's caching, and sheds the server round-trips that made the old pages feel sluggish. A typical "display related records" page collapses into something like:
// caseList.js — data via a cacheable Apex method, no full-page postbacks
import { LightningElement, wire } from 'lwc';
import getOpenCases from '@salesforce/apex/CaseController.getOpenCases';
export default class CaseList extends LightningElement {
@wire(getOpenCases) cases; // cached, reactive, no manual refresh plumbing
}
public with sharing class CaseController {
@AuraEnabled(cacheable=true)
public static List<Case> getOpenCases() {
return [
SELECT Id, CaseNumber, Subject, Priority
FROM Case WHERE IsClosed = false
WITH SECURITY_ENFORCED
ORDER BY CreatedDate DESC LIMIT 200
];
}
}
The cacheable=true annotation is quietly responsible for a lot of the perceived speed-up: repeat views are served from the client cache instead of hitting Apex again.
Process Builder → Flow consolidation
Overlapping Process Builders on the same object are a classic source of order-of-execution bugs and wasted CPU. Consolidating them into a single, well-ordered record-triggered Flow per object (split into before-save and after-save) reduces redundant DML and makes the automation actually readable. Before-save updates in Flow are dramatically cheaper than the equivalent Process Builder field updates.
De-risk the cutover
- Run side by side — enable Lightning for a pilot group while Classic stays available.
- Measure — capture page-load and Apex timings before and after, so "it feels faster" becomes a number.
- Train in context — in-app prompts and short guides beat a 40-page PDF nobody reads.
- Keep a rollback — feature flags and profile-based access let you revert a cohort instantly.
The payoff
Done incrementally, the migration delivered value every sprint instead of in one terrifying release — and the modernized components cut code complexity and improved system performance by 40%. Just as importantly, the org became maintainable again: Flows you can read, components you can reuse, and a UI users actually like.
Staring down a Classic org? Let's talk about a migration plan that won't keep you up at night.