The core mechanic is moving cost from read time to write time. In a normalized design the join cost is paid on every read. In a denormalized design you pay once when you write, by also updating the redundant copy, and every read after that is cheaper.
Several patterns fall under denormalization. Storing a derived column means keeping a value like order_total alongside the line items so you never re-sum them. Pre-joining means embedding fields from a parent row into a child row, like the customer_name example above. Pre-aggregation means maintaining a running counter such as comment_count on a post instead of running COUNT(*) every time. Materialized views are the database-managed version of the same idea: the engine stores the result of an expensive query and refreshes it on a schedule or on demand.
The hard part is consistency. When the source value changes, every copy must change too. You handle this with application code that updates both places in one transaction, with database triggers, with a background job that reconciles drift, or in event-driven systems with a stream that fans the update out to each copy. If you get this wrong, the customer renames themselves and old orders still show the old name.