A major Drupal upgrade is mostly dependency work. This is the sequence we follow.
1. Get to the latest minor release
Update core and contributed modules to the latest versions on your current major. Fix anything that breaks now, while the change set is small.
2. Run Upgrade Status
The Upgrade Status module scans custom and contributed code for deprecated API use and compatibility with the target version, and lists what blocks the upgrade.
3. Fix deprecations with Drupal Rector
Drupal Rector applies automated code changes for many deprecations in custom modules and themes. Review the diffs, then fix the rest by hand.
4. Update contributed modules
Switch each module to a release that supports the target version. Where none exists, look for a patch in the issue queue, a replacement, or consider whether you still need the feature.
5. Bump core with Composer
Require the new core packages with --update-with-all-dependencies, then run drush updb, drush cim and drush cr on staging.
6. Test and deploy
Run automated and manual regression tests, check logs for deprecation notices, and deploy with a rollback plan.
Doing this regularly keeps each upgrade a small task instead of a project.