Everything travels inside frames. Each frame has a 9-byte header giving its length, type, flags, and the stream it belongs to. Common types are HEADERS (carries the request or response metadata), DATA (carries the body), SETTINGS (connection-level config), and PUSH_PROMISE (server push). Because each frame is tagged with a stream ID, the receiver can pull pieces of many responses off one connection and reassemble each stream correctly.
Headers get compressed with HPACK. In HTTP/1.1 every request resent the same fat headers as plain text, often 500 to 800 bytes of cookies, user-agent, and accept lines on every single request. HPACK keeps a shared table of header fields on both ends, so after the first request a repeated header like the cookie can be sent as a one-byte index instead of hundreds of bytes. HPACK was also designed to resist the CRIME compression attack that broke earlier schemes.
Server push lets the server send a resource the client has not requested yet. When a browser asks for index.html, the server can push the CSS and JS it knows the page needs, saving a round trip. In practice push proved hard to use well because servers push things the browser already cached, so Chrome removed support for it in 2022 and most teams now use the cheaper preload hint instead.
Streams can carry priority weights and dependencies so the browser can tell the server that the CSS matters more than a footer image. Almost all HTTP/2 in the wild runs over TLS, since browsers only negotiate it over HTTPS using the ALPN extension during the handshake.