diff --git a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
index 2bf41e0f4c..93c8b57df1 100644
--- a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
+++ b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
@@ -25,6 +25,7 @@ class BlockConfigSchemaTest extends KernelTestBase {
     'block_content',
     'comment',
     'forum',
+    'history',
     'node',
     'statistics',
     // BlockManager->getModuleName() calls system_get_info().
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
index db255d79f8..4b4d25152b 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
@@ -20,6 +20,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal6TestBase {
     'block',
     'comment',
     'forum',
+    'history',
     'views',
     'block_content',
     'content_translation',
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
index 6ea998b0c9..c0ffc804bb 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
@@ -26,6 +26,7 @@ class MigrateBlockTest extends MigrateDrupal6TestBase {
     'aggregator',
     'book',
     'forum',
+    'history',
     'statistics',
   ];
 
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
index 3201d02b16..dacb779517 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
@@ -23,6 +23,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
     'comment',
     'filter',
     'forum',
+    'history',
     'views',
     'block_content',
     'config_translation',
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 051f335c80..b919bddeed 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -64,7 +64,7 @@
  * Comments changed after this time may be marked new, updated, or read,
  * depending on their state for the current user. Defaults to 30 days ago.
  *
- * @todo Remove when https://www.drupal.org/node/1029708 lands.
+ * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0.
  */
 define('COMMENT_NEW_LIMIT', REQUEST_TIME - 30 * 24 * 60 * 60);
 
diff --git a/core/modules/comment/src/CommentManager.php b/core/modules/comment/src/CommentManager.php
index f7458f4d57..598d34663f 100644
--- a/core/modules/comment/src/CommentManager.php
+++ b/core/modules/comment/src/CommentManager.php
@@ -217,20 +217,8 @@ public function getCountNewComments(EntityInterface $entity, $field_name = NULL,
     if ($this->currentUser->isAuthenticated() && $this->moduleHandler->moduleExists('history')) {
       // Retrieve the timestamp at which the current user last viewed this entity.
       if (!$timestamp) {
-        if ($entity->getEntityTypeId() == 'node') {
-          $timestamp = history_read($entity->id());
-        }
-        else {
-          $function = $entity->getEntityTypeId() . '_last_viewed';
-          if (function_exists($function)) {
-            $timestamp = $function($entity->id());
-          }
-          else {
-            // Default to 30 days ago.
-            // @todo Remove once https://www.drupal.org/node/1029708 lands.
-            $timestamp = COMMENT_NEW_LIMIT;
-          }
-        }
+        $timestamp = \Drupal::service('history.repository')->getLastViewed($entity->getEntityTypeId(), [$entity->id()], $this->currentUser);
+        $timestamp = $timestamp[$entity->id()];
       }
       $timestamp = ($timestamp > HISTORY_READ_LIMIT ? $timestamp : HISTORY_READ_LIMIT);
 
diff --git a/core/modules/comment/src/Plugin/views/field/NodeNewComments.php b/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
index b1a3de16f4..b24ae748ff 100644
--- a/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
+++ b/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
@@ -164,7 +164,7 @@ public function preRender(&$values) {
 
     if ($nids) {
       $result = $this->database->query("SELECT n.nid, COUNT(c.cid) as num_comments FROM {node} n INNER JOIN {comment_field_data} c ON n.nid = c.entity_id AND c.entity_type = 'node' AND c.default_langcode = 1
-        LEFT JOIN {history} h ON h.nid = n.nid AND h.uid = :h_uid WHERE n.nid IN ( :nids[] )
+        LEFT JOIN {history} h ON h.entity_id = n.nid AND h.uid = :h_uid WHERE n.nid IN ( :nids[] )
         AND c.changed > GREATEST(COALESCE(h.timestamp, :timestamp1), :timestamp2) AND c.status = :status GROUP BY n.nid", [
         ':status' => CommentInterface::PUBLISHED,
         ':h_uid' => $user->id(),
diff --git a/core/modules/comment/tests/src/Functional/CommentLinksTest.php b/core/modules/comment/tests/src/Functional/CommentLinksTest.php
index 4ce4e1c195..1d3f00635e 100644
--- a/core/modules/comment/tests/src/Functional/CommentLinksTest.php
+++ b/core/modules/comment/tests/src/Functional/CommentLinksTest.php
@@ -77,6 +77,10 @@ public function testCommentLinks() {
     $this->node->comment = CommentItemInterface::OPEN;
     $this->node->save();
 
+    // comment_num_new() relies on history_read(), so ensure that no one has
+    // seen the node of this comment.
+    \Drupal::service('history.repository')->deleteByEntity($this->node);
+
     // Change user permissions.
     $perms = [
       'access comments' => 1,
diff --git a/core/modules/forum/forum.services.yml b/core/modules/forum/forum.services.yml
index 6268c2cf8a..a83af7fea2 100644
--- a/core/modules/forum/forum.services.yml
+++ b/core/modules/forum/forum.services.yml
@@ -1,7 +1,7 @@
 services:
   forum_manager:
     class: Drupal\forum\ForumManager
-    arguments: ['@config.factory', '@entity_type.manager', '@database', '@string_translation', '@comment.manager', '@entity_field.manager']
+    arguments: ['@config.factory', '@entity_type.manager', '@database', '@string_translation', '@comment.manager', '@entity_field.manager', '@history.repository']
     tags:
       - { name: backend_overridable }
   forum.breadcrumb.node:
diff --git a/core/modules/forum/src/ForumManager.php b/core/modules/forum/src/ForumManager.php
index 29e03e1cb7..33ca731cc6 100644
--- a/core/modules/forum/src/ForumManager.php
+++ b/core/modules/forum/src/ForumManager.php
@@ -13,6 +13,7 @@
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\comment\CommentManagerInterface;
 use Drupal\node\NodeInterface;
+use Drupal\history\HistoryRepositoryInterface;
 
 /**
  * Provides forum manager service.
@@ -120,6 +121,13 @@ class ForumManager implements ForumManagerInterface {
    */
   protected $index;
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
   /**
    * Constructs the forum manager service.
    *
@@ -135,8 +143,10 @@ class ForumManager implements ForumManagerInterface {
    *   The comment manager service.
    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
    *   The entity field manager.
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager = NULL) {
+  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager = NULL, HistoryRepositoryInterface $history_repository = NULL) {
     $this->configFactory = $config_factory;
     $this->entityTypeManager = $entity_type_manager;
     $this->connection = $connection;
@@ -147,6 +157,11 @@ public function __construct(ConfigFactoryInterface $config_factory, EntityTypeMa
       $entity_field_manager = \Drupal::service('entity_field.manager');
     }
     $this->entityFieldManager = $entity_field_manager;
+    if (!$history_repository) {
+      @trigger_error('The history.repository service must be passed to ForumManager::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2081585.', E_USER_DEPRECATED);
+      $history_repository = \Drupal::service('history.repository');
+    }
+    $this->historyRepository = $history_repository;
   }
 
   /**
@@ -329,16 +344,11 @@ protected function getTopicOrder($sortby) {
    *   previously viewed the node; otherwise HISTORY_READ_LIMIT.
    */
   protected function lastVisit($nid, AccountInterface $account) {
+    $this->history += $this->historyRepository->getLastViewed('node', [$nid], $account);
     if (empty($this->history[$nid])) {
-      $result = $this->connection->select('history', 'h')
-        ->fields('h', ['nid', 'timestamp'])
-        ->condition('uid', $account->id())
-        ->execute();
-      foreach ($result as $t) {
-        $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
-      }
+      $this->history[$nid] = HISTORY_READ_LIMIT;
     }
-    return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT;
+    return $this->history[$nid];
   }
 
   /**
@@ -498,7 +508,7 @@ public function checkNodeType(NodeInterface $node) {
   public function unreadTopics($term, $uid) {
     $query = $this->connection->select('node_field_data', 'n');
     $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $term]);
-    $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', [':uid' => $uid]);
+    $query->leftJoin('history', 'h', "n.nid = h.entity_id AND h.entity_type = 'node' AND h.uid = :uid", [':uid' => $uid]);
     $query->addExpression('COUNT(n.nid)', 'count');
     return $query
       ->condition('status', 1)
@@ -506,7 +516,7 @@ public function unreadTopics($term, $uid) {
       //   field language and just fall back to the default language.
       ->condition('n.default_langcode', 1)
       ->condition('n.created', HISTORY_READ_LIMIT, '>')
-      ->isNull('h.nid')
+      ->isNull('h.entity_id')
       ->addTag('node_access')
       ->execute()
       ->fetchField();
diff --git a/core/modules/forum/tests/src/Kernel/ForumValidationTest.php b/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
index 9b9661b5a2..01664be733 100644
--- a/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
+++ b/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
@@ -18,7 +18,7 @@ class ForumValidationTest extends EntityKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['node', 'options', 'comment', 'taxonomy', 'forum'];
+  public static $modules = ['node', 'options', 'comment', 'taxonomy', 'history', 'forum'];
 
   /**
    * Tests the forum validation constraints.
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
index 302c3d1aee..26f56ee2cd 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
@@ -17,7 +17,7 @@ class MigrateForumConfigsTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['comment', 'forum', 'taxonomy'];
+  public static $modules = ['comment', 'forum', 'history', 'taxonomy'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
index 6f5d48c1dd..6141d5700b 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
@@ -22,6 +22,7 @@ class MigrateForumTest extends MigrateNodeTestBase {
   public static $modules = [
     'comment',
     'forum',
+    'history',
     'menu_ui',
     'taxonomy',
   ];
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
index cd1c7fc3ea..c1b33410ed 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
@@ -19,6 +19,7 @@ class MigrateForumSettingsTest extends MigrateDrupal7TestBase {
     'text',
     'node',
     'taxonomy',
+    'history',
     'forum',
   ];
 
diff --git a/core/modules/forum/tests/src/Unit/ForumManagerTest.php b/core/modules/forum/tests/src/Unit/ForumManagerTest.php
index 969bb71219..85b85f5c9a 100644
--- a/core/modules/forum/tests/src/Unit/ForumManagerTest.php
+++ b/core/modules/forum/tests/src/Unit/ForumManagerTest.php
@@ -56,6 +56,10 @@ public function testGetIndex() {
       ->disableOriginalConstructor()
       ->getMock();
 
+    $history_repository = $this->getMockBuilder('\Drupal\history\HistoryRepository')
+      ->disableOriginalConstructor()
+      ->getMock();
+
     $comment_manager = $this->getMockBuilder('\Drupal\comment\CommentManagerInterface')
       ->disableOriginalConstructor()
       ->getMock();
@@ -69,6 +73,7 @@ public function testGetIndex() {
         $translation_manager,
         $comment_manager,
         $entity_field_manager,
+        $history_repository,
       ])
       ->getMock();
 
diff --git a/core/modules/history/history.install b/core/modules/history/history.install
index 85e2fdabbf..e794cfbc49 100644
--- a/core/modules/history/history.install
+++ b/core/modules/history/history.install
@@ -6,22 +6,30 @@
  */
 
 use Drupal\Core\Database\Database;
+use Drupal\Core\Entity\EntityTypeInterface;
 
 /**
  * Implements hook_schema().
  */
 function history_schema() {
   $schema['history'] = [
-    'description' => 'A record of which {users} have read which {node}s.',
+    'description' => 'A record of which {users} have read which entities.',
     'fields' => [
       'uid' => [
-        'description' => 'The {users}.uid that read the {node} nid.',
+        'description' => 'The {users}.uid that read the {history}.entity_id entity.',
         'type' => 'int',
         'not null' => TRUE,
         'default' => 0,
       ],
-      'nid' => [
-        'description' => 'The {node}.nid that was read.',
+      'entity_type' => [
+        'description' => 'The type of the entity that was read.',
+        'type' => 'varchar_ascii',
+        'not null' => TRUE,
+        'default' => 'node',
+        'length' => EntityTypeInterface::ID_MAX_LENGTH,
+      ],
+      'entity_id' => [
+        'description' => 'The ID of the entity that was read.',
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
@@ -34,9 +42,9 @@ function history_schema() {
         'default' => 0,
       ],
     ],
-    'primary key' => ['uid', 'nid'],
+    'primary key' => ['entity_type', 'uid', 'entity_id'],
     'indexes' => [
-      'nid' => ['nid'],
+      'history_entity' => ['entity_type', 'entity_id'],
     ],
   ];
 
@@ -88,3 +96,63 @@ function history_update_8101() {
   ];
   $schema->addIndex('history', 'nid', ['nid'], $spec);
 }
+
+/**
+ * Change {history}.nid to {history}.entity_id and add {history}.entity_type.
+ */
+function history_update_8802() {
+  $schema = Database::getConnection()->schema();
+  $schema->dropPrimaryKey('history');
+  $schema->dropIndex('history', 'nid');
+  $schema->changeField('history', 'nid', 'entity_id', [
+    'description' => 'The ID of the entity that was read.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 0,
+  ]);
+  $schema->addField('history', 'entity_type', [
+    'description' => 'The type of the entity that was read.',
+    'type' => 'varchar_ascii',
+    'not null' => TRUE,
+    'default' => 'node',
+    'length' => EntityTypeInterface::ID_MAX_LENGTH,
+  ]);
+  $schema->addPrimaryKey('history', ['entity_type', 'uid', 'entity_id']);
+  $spec = [
+    'description' => 'A record of which {users} have read which entities.',
+    'fields' => [
+      'uid' => [
+        'description' => 'The {users}.uid that read the {history}.entity_id entity.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'entity_type' => [
+        'description' => 'The type of the entity that was read.',
+        'type' => 'varchar_ascii',
+        'not null' => TRUE,
+        'default' => 'node',
+        'length' => EntityTypeInterface::ID_MAX_LENGTH,
+      ],
+      'entity_id' => [
+        'description' => 'The ID of the entity that was read.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'timestamp' => [
+        'description' => 'The Unix timestamp at which the read occurred.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+    ],
+    'primary key' => ['entity_type', 'uid', 'entity_id'],
+    'indexes' => [
+      'history_entity' => ['entity_type', 'entity_id'],
+    ],
+  ];
+  $schema->addIndex('history', 'history_entity', ['entity_type', 'entity_id'], $spec);
+}
diff --git a/core/modules/history/history.module b/core/modules/history/history.module
index 3f73c2dc02..c0136dc31a 100644
--- a/core/modules/history/history.module
+++ b/core/modules/history/history.module
@@ -15,6 +15,7 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\history\HistoryRenderCallback;
 use Drupal\user\UserInterface;
+use Drupal\Core\Session\AccountInterface;
 
 /**
  * Entities changed before this time are always shown as read.
@@ -45,9 +46,12 @@ function history_help($route_name, RouteMatchInterface $route_match) {
  * @return int
  *   If a node has been previously viewed by the user, the timestamp in seconds
  *   of when the last view occurred; otherwise, zero.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::getLastViewed() instead.
  */
 function history_read($nid) {
-  $history = history_read_multiple([$nid]);
+  $account = \Drupal::currentUser();
+  $history = \Drupal::service('history.repository')->getLastViewed('node', [$nid], $account);
   return $history[$nid];
 }
 
@@ -61,37 +65,12 @@ function history_read($nid) {
  *   Array of timestamps keyed by node ID. If a node has been previously viewed
  *   by the user, the timestamp in seconds of when the last view occurred;
  *   otherwise, zero.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::getLastViewed() instead.
  */
 function history_read_multiple($nids) {
-  $history = &drupal_static(__FUNCTION__, []);
-
-  $return = [];
-
-  $nodes_to_read = [];
-  foreach ($nids as $nid) {
-    if (isset($history[$nid])) {
-      $return[$nid] = $history[$nid];
-    }
-    else {
-      // Initialize value if current user has not viewed the node.
-      $nodes_to_read[$nid] = 0;
-    }
-  }
-
-  if (empty($nodes_to_read)) {
-    return $return;
-  }
-
-  $result = \Drupal::database()->query('SELECT nid, timestamp FROM {history} WHERE uid = :uid AND nid IN ( :nids[] )', [
-    ':uid' => \Drupal::currentUser()->id(),
-    ':nids[]' => array_keys($nodes_to_read),
-  ]);
-  foreach ($result as $row) {
-    $nodes_to_read[$row->nid] = (int) $row->timestamp;
-  }
-  $history += $nodes_to_read;
-
-  return $return + $nodes_to_read;
+  $account = \Drupal::currentUser();
+  return \Drupal::service('history.repository')->getLastViewed('node', $nids, $account);
 }
 
 /**
@@ -99,37 +78,26 @@ function history_read_multiple($nids) {
  *
  * @param $nid
  *   The node ID that has been read.
- * @param $account
+ * @param \Drupal\Core\Session\AccountInterface $account
  *   (optional) The user account to update the history for. Defaults to the
  *   current user.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::updateLastViewed() instead.
  */
-function history_write($nid, $account = NULL) {
+function history_write($nid, AccountInterface $account = NULL) {
 
   if (!isset($account)) {
     $account = \Drupal::currentUser();
   }
-
-  if ($account->isAuthenticated()) {
-    \Drupal::database()->merge('history')
-      ->keys([
-        'uid' => $account->id(),
-        'nid' => $nid,
-      ])
-      ->fields(['timestamp' => REQUEST_TIME])
-      ->execute();
-    // Update static cache.
-    $history = &drupal_static('history_read_multiple', []);
-    $history[$nid] = REQUEST_TIME;
-  }
+  $node = entity_load('node', $nid);
+  \Drupal::service('history.repository')->updateLastViewed($node, $account);
 }
 
 /**
  * Implements hook_cron().
  */
 function history_cron() {
-  \Drupal::database()->delete('history')
-    ->condition('timestamp', HISTORY_READ_LIMIT, '<')
-    ->execute();
+  \Drupal::service('history.repository')->purge();
 }
 
 /**
@@ -154,9 +122,7 @@ function history_node_view_alter(array &$build, EntityInterface $node, EntityVie
  * Implements hook_ENTITY_TYPE_delete() for node entities.
  */
 function history_node_delete(EntityInterface $node) {
-  \Drupal::database()->delete('history')
-    ->condition('nid', $node->id())
-    ->execute();
+  \Drupal::service('history.repository')->deleteByEntity($node);
 }
 
 /**
@@ -165,9 +131,7 @@ function history_node_delete(EntityInterface $node) {
 function history_user_cancel($edit, UserInterface $account, $method) {
   switch ($method) {
     case 'user_cancel_reassign':
-      \Drupal::database()->delete('history')
-        ->condition('uid', $account->id())
-        ->execute();
+      \Drupal::service('history.repository')->deleteByUser($account);
       break;
   }
 }
@@ -176,9 +140,7 @@ function history_user_cancel($edit, UserInterface $account, $method) {
  * Implements hook_ENTITY_TYPE_delete() for user entities.
  */
 function history_user_delete($account) {
-  \Drupal::database()->delete('history')
-    ->condition('uid', $account->id())
-    ->execute();
+  \Drupal::service('history.repository')->deleteByUser($account);
 }
 
 /**
diff --git a/core/modules/history/history.module.orig b/core/modules/history/history.module.orig
new file mode 100644
index 0000000000..3f73c2dc02
--- /dev/null
+++ b/core/modules/history/history.module.orig
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @file
+ * Records which users have read which content.
+ *
+ * @todo
+ * - Generic helper for _forum_user_last_visit() + history_read().
+ * - Generic helper for node_mark().
+ */
+
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\history\HistoryRenderCallback;
+use Drupal\user\UserInterface;
+
+/**
+ * Entities changed before this time are always shown as read.
+ *
+ * Entities changed within this time may be marked as new, updated, or read,
+ * depending on their state for the current user. Defaults to 30 days ago.
+ */
+define('HISTORY_READ_LIMIT', REQUEST_TIME - 30 * 24 * 60 * 60);
+
+/**
+ * Implements hook_help().
+ */
+function history_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.history':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The History module keeps track of which content a user has read. It marks content as <em>new</em> or <em>updated</em> depending on the last time the user viewed it. History records that are older than one month are removed during cron, which means that content older than one month is always considered <em>read</em>. The History module does not have a user interface but it provides a filter to <a href=":views-help">Views</a> to show new or updated content. For more information, see the <a href=":url">online documentation for the History module</a>.', [':views-help' => (\Drupal::moduleHandler()->moduleExists('views')) ? Url::fromRoute('help.page', ['name' => 'views'])->toString() : '#', ':url' => 'https://www.drupal.org/documentation/modules/history']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Retrieves the timestamp for the current user's last view of a specified node.
+ *
+ * @param int $nid
+ *   A node ID.
+ *
+ * @return int
+ *   If a node has been previously viewed by the user, the timestamp in seconds
+ *   of when the last view occurred; otherwise, zero.
+ */
+function history_read($nid) {
+  $history = history_read_multiple([$nid]);
+  return $history[$nid];
+}
+
+/**
+ * Retrieves the last viewed timestamp for each of the passed node IDs.
+ *
+ * @param array $nids
+ *   An array of node IDs.
+ *
+ * @return array
+ *   Array of timestamps keyed by node ID. If a node has been previously viewed
+ *   by the user, the timestamp in seconds of when the last view occurred;
+ *   otherwise, zero.
+ */
+function history_read_multiple($nids) {
+  $history = &drupal_static(__FUNCTION__, []);
+
+  $return = [];
+
+  $nodes_to_read = [];
+  foreach ($nids as $nid) {
+    if (isset($history[$nid])) {
+      $return[$nid] = $history[$nid];
+    }
+    else {
+      // Initialize value if current user has not viewed the node.
+      $nodes_to_read[$nid] = 0;
+    }
+  }
+
+  if (empty($nodes_to_read)) {
+    return $return;
+  }
+
+  $result = \Drupal::database()->query('SELECT nid, timestamp FROM {history} WHERE uid = :uid AND nid IN ( :nids[] )', [
+    ':uid' => \Drupal::currentUser()->id(),
+    ':nids[]' => array_keys($nodes_to_read),
+  ]);
+  foreach ($result as $row) {
+    $nodes_to_read[$row->nid] = (int) $row->timestamp;
+  }
+  $history += $nodes_to_read;
+
+  return $return + $nodes_to_read;
+}
+
+/**
+ * Updates 'last viewed' timestamp of the specified entity for the current user.
+ *
+ * @param $nid
+ *   The node ID that has been read.
+ * @param $account
+ *   (optional) The user account to update the history for. Defaults to the
+ *   current user.
+ */
+function history_write($nid, $account = NULL) {
+
+  if (!isset($account)) {
+    $account = \Drupal::currentUser();
+  }
+
+  if ($account->isAuthenticated()) {
+    \Drupal::database()->merge('history')
+      ->keys([
+        'uid' => $account->id(),
+        'nid' => $nid,
+      ])
+      ->fields(['timestamp' => REQUEST_TIME])
+      ->execute();
+    // Update static cache.
+    $history = &drupal_static('history_read_multiple', []);
+    $history[$nid] = REQUEST_TIME;
+  }
+}
+
+/**
+ * Implements hook_cron().
+ */
+function history_cron() {
+  \Drupal::database()->delete('history')
+    ->condition('timestamp', HISTORY_READ_LIMIT, '<')
+    ->execute();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_view_alter() for node entities.
+ */
+function history_node_view_alter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
+  // Update the history table, stating that this user viewed this node.
+  if ($display->getOriginalMode() === 'full') {
+    $build['#cache']['contexts'][] = 'user.roles:authenticated';
+    if (\Drupal::currentUser()->isAuthenticated()) {
+      // When the window's "load" event is triggered, mark the node as read.
+      // This still allows for Drupal behaviors (which are triggered on the
+      // "DOMContentReady" event) to add "new" and "updated" indicators.
+      $build['#attached']['library'][] = 'history/mark-as-read';
+      $build['#attached']['drupalSettings']['history']['nodesToMarkAsRead'][$node->id()] = TRUE;
+    }
+  }
+
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for node entities.
+ */
+function history_node_delete(EntityInterface $node) {
+  \Drupal::database()->delete('history')
+    ->condition('nid', $node->id())
+    ->execute();
+}
+
+/**
+ * Implements hook_user_cancel().
+ */
+function history_user_cancel($edit, UserInterface $account, $method) {
+  switch ($method) {
+    case 'user_cancel_reassign':
+      \Drupal::database()->delete('history')
+        ->condition('uid', $account->id())
+        ->execute();
+      break;
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for user entities.
+ */
+function history_user_delete($account) {
+  \Drupal::database()->delete('history')
+    ->condition('uid', $account->id())
+    ->execute();
+}
+
+/**
+ * #lazy_builder callback; attaches the last read timestamp for a node.
+ *
+ * @param int $node_id
+ *   The node ID for which to attach the last read timestamp.
+ *
+ * @return array
+ *   A renderable array containing the last read timestamp.
+ *
+ * @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use
+ *   \Drupal\history\HistoryRenderCallback::lazyBuilder() instead.
+ *
+ * @see https://www.drupal.org/node/2966725
+ */
+function history_attach_timestamp($node_id) {
+  @trigger_error('history_attach_timestamp() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\history\HistoryRenderCallback::lazyBuilder() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED);
+  return HistoryRenderCallback::lazyBuilder($node_id);
+}
diff --git a/core/modules/history/history.module.rej b/core/modules/history/history.module.rej
new file mode 100644
index 0000000000..3b697e5343
--- /dev/null
+++ b/core/modules/history/history.module.rej
@@ -0,0 +1,17 @@
+***************
+*** 192,197 ****
+   */
+  function history_attach_timestamp($node_id) {
+    $element = [];
+-   $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = (int) history_read($node_id);
+    return $element;
+  }
+--- 154,161 ----
+   */
+  function history_attach_timestamp($node_id) {
+    $element = [];
++   $account = \Drupal::currentUser();
++   $history = \Drupal::service('history.repository')->getLastViewed('node', [$node_id], $account);
++   $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = $history;
+    return $element;
+  }
diff --git a/core/modules/history/history.services.yml b/core/modules/history/history.services.yml
new file mode 100644
index 0000000000..669cd0a618
--- /dev/null
+++ b/core/modules/history/history.services.yml
@@ -0,0 +1,6 @@
+services:
+  history.repository:
+    class: Drupal\history\HistoryRepository
+    arguments: ['@database']
+    tags:
+      - { name: backend_overridable }
diff --git a/core/modules/history/history.views.inc b/core/modules/history/history.views.inc
index 60c95cd868..4df607754f 100644
--- a/core/modules/history/history.views.inc
+++ b/core/modules/history/history.views.inc
@@ -22,8 +22,9 @@ function history_views_data() {
     'node_field_data' => [
       'table' => 'history',
       'left_field' => 'nid',
-      'field' => 'nid',
+      'field' => 'entity_id',
       'extra' => [
+        ['field' => 'entity_type', 'value' => 'node'],
         ['field' => 'uid', 'value' => '***CURRENT_USER***', 'numeric' => TRUE],
       ],
     ],
diff --git a/core/modules/history/src/Controller/HistoryController.php b/core/modules/history/src/Controller/HistoryController.php
index c6a4834903..bf50377994 100644
--- a/core/modules/history/src/Controller/HistoryController.php
+++ b/core/modules/history/src/Controller/HistoryController.php
@@ -2,11 +2,13 @@
 
 namespace Drupal\history\Controller;
 
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\node\NodeInterface;
 
 /**
@@ -14,13 +16,39 @@
  */
 class HistoryController extends ControllerBase {
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
+  /**
+   * Constructs a HistoryController object.
+   *
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
+   */
+  public function __construct(HistoryRepositoryInterface $history_repository) {
+    $this->historyRepository = $history_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('history.repository')
+    );
+  }
+
   /**
    * Returns a set of nodes' last read timestamps.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request of the page.
    *
-   * @return Symfony\Component\HttpFoundation\JsonResponse
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   The JSON response.
    */
   public function getNodeReadTimestamps(Request $request) {
@@ -32,7 +60,7 @@ public function getNodeReadTimestamps(Request $request) {
     if (!isset($nids)) {
       throw new NotFoundHttpException();
     }
-    return new JsonResponse(history_read_multiple($nids));
+    return new JsonResponse($this->historyRepository->getLastViewed('node', $nids, $this->currentUser()));
   }
 
   /**
@@ -42,6 +70,8 @@ public function getNodeReadTimestamps(Request $request) {
    *   The request of the page.
    * @param \Drupal\node\NodeInterface $node
    *   The node whose "last read" timestamp should be updated.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function readNode(Request $request, NodeInterface $node) {
     if ($this->currentUser()->isAnonymous()) {
@@ -49,9 +79,10 @@ public function readNode(Request $request, NodeInterface $node) {
     }
 
     // Update the history table, stating that this user viewed this node.
-    history_write($node->id());
+    $this->historyRepository->updateLastViewed($node, $this->currentUser());
 
-    return new JsonResponse((int) history_read($node->id()));
+    $history = $this->historyRepository->getLastViewed('node', [$node->id()], $this->currentUser());
+    return new JsonResponse($history[$node->id()]);
   }
 
 }
diff --git a/core/modules/history/src/HistoryRepository.php b/core/modules/history/src/HistoryRepository.php
new file mode 100644
index 0000000000..36c1817f30
--- /dev/null
+++ b/core/modules/history/src/HistoryRepository.php
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\history\HistoryRepository.
+ */
+
+namespace Drupal\history;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides history repository service.
+ */
+class HistoryRepository implements HistoryRepositoryInterface {
+  use DependencySerializationTrait {
+    __wakeup as defaultWakeup;
+    __sleep as defaultSleep;
+  }
+
+  /**
+   * Array of history keyed by entity type and entity id.
+   *
+   * @var array
+   */
+  protected $history = [];
+
+  /**
+   * Database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs the history repository service.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastViewed($entity_type, $entity_ids, AccountInterface $account) {
+    $return = [];
+
+    $entities_to_read = [];
+    foreach ($entity_ids as $entity_id) {
+      if (isset($this->history[$entity_type][$account->id()][$entity_id])) {
+        $return[$entity_id] = $this->history[$entity_type][$account->id()][$entity_id];
+      }
+      else {
+        $entities_to_read[$entity_id] = 0;
+      }
+    }
+
+    if (empty($entities_to_read)) {
+      return $return;
+    }
+
+    $result = $this->connection->select('history', 'h')
+      ->fields('h', ['entity_id', 'timestamp'])
+      ->condition('uid', $account->id())
+      ->condition('entity_type', $entity_type)
+      ->condition('entity_id', array_keys($entities_to_read), 'IN')
+      ->execute();
+
+    foreach ($result as $row) {
+      $entities_to_read[$row->entity_id] = (int) $row->timestamp;
+    }
+    if (!isset($this->history[$entity_type][$account->id()])) {
+      $this->history[$entity_type][$account->id()] = [];
+    }
+    $this->history[$entity_type][$account->id()] += $entities_to_read;
+
+    return $return + $entities_to_read;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLastViewed(EntityInterface $entity, AccountInterface $account) {
+    if ($account->isAuthenticated()) {
+      $this->connection->merge('history')
+        ->keys([
+          'uid' => $account->id(),
+          'entity_id' => $entity->id(),
+          'entity_type' => $entity->getEntityTypeId(),
+        ])
+        ->fields(['timestamp' => REQUEST_TIME])
+        ->execute();
+      // Update cached value.
+      $this->history[$entity->getEntityTypeId()][$account->id()][$entity->id()] = REQUEST_TIME;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function purge() {
+    $this->connection->delete('history')
+      ->condition('timestamp', HISTORY_READ_LIMIT, '<')
+      ->execute();
+    // Clean static cache.
+    $this->resetCache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteByUser(AccountInterface $account) {
+    $this->connection->delete('history')
+      ->condition('uid', $account->id())
+      ->execute();
+    // Clean static cache.
+    $this->resetCache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteByEntity(EntityInterface $entity) {
+    $this->connection->delete('history')
+      ->condition('entity_id', $entity->id())
+      ->condition('entity_type', $entity->getEntityTypeId())
+      ->execute();
+    // Clean static cache.
+    $this->resetCache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetCache() {
+    $this->history = [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __sleep() {
+    $vars = $this->defaultSleep();
+    // Do not serialize static cache.
+    unset($vars['history']);
+    return $vars;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __wakeup() {
+    $this->defaultWakeup();
+    // Initialize static cache.
+    $this->history = [];
+  }
+
+}
diff --git a/core/modules/history/src/HistoryRepositoryInterface.php b/core/modules/history/src/HistoryRepositoryInterface.php
new file mode 100644
index 0000000000..7f9f0dcde0
--- /dev/null
+++ b/core/modules/history/src/HistoryRepositoryInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\history\HistoryRepositoryInterface.
+ */
+
+namespace Drupal\history;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines an interface to store and retrieve a last view timestamp of entities.
+ */
+interface HistoryRepositoryInterface {
+
+  /**
+   * Retrieves the timestamp for the current user's last view of the entities.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   * @param array $entity_ids
+   *   The entity IDs.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to get the history for.
+   *
+   * @return array
+   *   Array of timestamps keyed by entity ID. If a entity has been previously
+   *   viewed by the user, the timestamp in seconds of when the last view
+   *   occurred; otherwise, zero.
+   */
+  public function getLastViewed($entity_type, $entity_ids, AccountInterface $account);
+
+  /**
+   * Updates 'last viewed' timestamp of the entity for the user account.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that history should be updated.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to update the history for.
+   */
+  public function updateLastViewed(EntityInterface $entity, AccountInterface $account);
+
+  /**
+   * Purges outdated history.
+   */
+  public function purge();
+
+  /**
+   * Deletes history of the user account.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to purge history.
+   */
+  public function deleteByUser(AccountInterface $account);
+
+  /**
+   * Deletes history for the given entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that history should be deleted.
+   */
+  public function deleteByEntity(EntityInterface $entity);
+
+  /**
+   * Resets the static cache.
+   */
+  public function resetCache();
+
+}
diff --git a/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php b/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
index f66ce52476..7f9b151c35 100644
--- a/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
+++ b/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
@@ -72,14 +72,16 @@ public function testHandlers() {
     $connection->insert('history')
       ->fields([
         'uid' => $account->id(),
-        'nid' => $nodes[0]->id(),
+        'entity_id' => $nodes[0]->id(),
+        'entity_type' => 'node',
         'timestamp' => REQUEST_TIME - 100,
       ])->execute();
 
     $connection->insert('history')
       ->fields([
         'uid' => $account->id(),
-        'nid' => $nodes[1]->id(),
+        'entity_id' => $nodes[1]->id(),
+        'entity_type' => 'node',
         'timestamp' => REQUEST_TIME + 100,
       ])->execute();
 
diff --git a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
index 1a340e4e43..9612bd5e16 100644
--- a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
+++ b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
@@ -39,6 +39,7 @@ class MigrationPluginListTest extends KernelTestBase {
     'file',
     'filter',
     'forum',
+    'history',
     'image',
     'language',
     'locale',
