Session History

A browser's session history keeps track of the navigations in each tab, to support back/forward navigations and session restore. This is in contrast to “history” (e.g., chrome://history), which tracks the main frame URLs the user has visited in any tab for the lifetime of a profile.

Chromium tracks the session history of each tab in NavigationController, using a list of NavigationEntry objects to represent the joint session history items. Each frame creates session history items as it navigates. A joint session history item contains the state of each frame of a page at a given point in time, including things like URL, partially entered form data, scroll position, etc. Each NavigationEntry uses a tree of FrameNavigationEntries to track this state.

Pruning Forward Navigations

If the user goes back and then commits a new navigation, this essentially forks the joint session history. However, joint session history is tracked as a list and not as a tree, so the previous forward history is “pruned” and forgotten. This pruning is performed for all new navigations, unless they commit with replacement.

Subframe Navigations

When the first commit occurs within a new subframe of a document, it becomes part of the existing joint session history item (which we refer to as an “auto subframe navigation”). The user can't go back to the state before the frame committed. Any subsequent navigations in the subframe create new joint session history items (which we refer to as “manual subframe navigations”), such that clicking back goes back within the subframe.

Navigating with Replacement

Some types of navigations can replace the previously committed joint session history item for a frame, rather than creating a new item. These include:

  • location.replace (which is usually cross-document, unless it is a fragment navigation)
  • history.replaceState (which is always same-document)
  • Client redirects
  • The first non-blank URL after the initial empty document (unless the frame was explicitly created with about:blank as the URL).

Identifying Same- and Cross-Document Navigations

Each FrameNavigationEntry contains both an item sequence number (ISN) and a document sequence number (DSN). Same-document navigations create a new session history item without changing the document, and thus have a new ISN but the same DSN. Cross-document navigations create a new ISN and DSN. NavigationController uses these ISNs and DSNs when deciding which frames need to be navigated during a session history navigation, using a recursive frame tree walk in FindFramesToNavigate.

Classifying Navigations

Much of the complexity in NavigationController comes from the bookkeeping needed to track the various types of navigations as they commit (e.g., same-document vs cross-document, main frame vs subframe, with or without replacement, etc). These types may lead to different outcomes for whether a new NavigationEntry is created, whether an existing one is updated vs replaced, and what events are exposed to observers. This is handled by ClassifyNavigation, which determines which RendererDidNavigate helper methods are used when a navigation commits.

Persistence

The joint session history of a tab is persisted so that tabs can be restored (e.g., between Chromium restarts, after closing a tab, or on another device). This requires serializing the state in each NavigationEntry and its tree of FrameNavigationEntries, using a PageState object and other metadata.

Not everything in NavigationEntry is persisted. All data members of NavigationEntryImpl and FrameNavigationEntry should be documented with whether they are preserved after commit and whether they need to be persisted.

Note that the session history of a tab can also be cloned when duplicating a tab, or when doing a back/forward/reload navigation in a new tab (such as when middle-clicking the back/forward/reload button). This involves direct clones of NavigationEntries rather than persisting and restoring.

Invariants

  • The pending_entry_index_ is either -1 or an index into entries_. If pending_entry_ is defined and pending_entry_index_ is -1, then it is a new navigation. If pending_entry_index_ is a valid index into entries_, then pending_entry_ must point to that entry and it is a session history navigation.
  • Newly created tabs have NavigationControllers with is_initial_navigation_ set to true. They can have last_committed_entry_index_ defined before the first commit, however, when session history is cloned from another tab. (In this case, pending_entry_index_ indicates which entry is going to be restored during the initial navigation.)
  • Every FrameNavigationEntry that has committed in the current session (as opposed to those that have been restored) must have a SiteInstance.
  • A renderer process can only update FrameNavigationEntries belonging to a SiteInstance in that process. This especially includes attacker-controlled data like PageState, which could be dangerous to load into a different site's process.
  • Any cross-SiteInstance navigation should result in a new NavigationEntry with replacement, rather than updating an existing NavigationEntry.

Caveats

  • Not every NavigationRequest has a pending NavigationEntry. For example, subframe navigations do not, and renderer-initiated main frame navigations may clear an existing browser-initiated pending NavigationEntry (using PendingEntryRef) without replacing it with a new one.
  • Some main frame documents may not have a corresponding NavigationEntry even after commit (e.g., the initial empty document, per issue 524208).
  • Some subframe documents may not have a corresponding FrameNavigationEntry after commit (e.g., see issue 608402).
  • FrameNavigationEntries should be shared between NavigationEntries when they do not change (e.g., to support history.replaceState after subframe navigations), but this is not yet supported. See issue 373041.