diff --git a/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..32d28726181c0a721b206c1768aff8d929433d27
--- /dev/null
+++ b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\statistics;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\State\StateInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides the default database storage backend for statistics.
+ */
+class NodeStatisticsDatabaseStorage implements StatisticsStorageInterface {
+
+  /**
+  * The database connection used.
+  *
+  * @var \Drupal\Core\Database\Connection
+  */
+  protected $connection;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs the statistics storage.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection for the node view storage.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
+   */
+  public function __construct(Connection $connection, StateInterface $state, RequestStack $request_stack) {
+    $this->connection = $connection;
+    $this->state = $state;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function recordView($id) {
+    return (bool) $this->connection
+      ->merge('node_counter')
+      ->key('nid', $id)
+      ->fields([
+        'daycount' => 1,
+        'totalcount' => 1,
+        'timestamp' => $this->getRequestTime(),
+      ])
+      ->expression('daycount', 'daycount + 1')
+      ->expression('totalcount', 'totalcount + 1')
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchViews($ids) {
+    $views = $this->connection
+      ->select('node_counter', 'nc')
+      ->fields('nc', ['totalcount', 'daycount', 'timestamp'])
+      ->condition('nid', $ids, 'IN')
+      ->execute()
+      ->fetchAll();
+    foreach ($views as $id => $view) {
+      $views[$id] = new StatisticsViewsResult($view->totalcount, $view->daycount, $view->timestamp);
+    }
+    return $views;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchView($id) {
+    $views = $this->fetchViews(array($id));
+    return reset($views);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchAll($order = 'totalcount', $limit = 5) {
+    assert(in_array($order, ['totalcount', 'daycount', 'timestamp']), "Invalid order argument.");
+
+    return $this->connection
+      ->select('node_counter', 'nc')
+      ->fields('nc', ['nid'])
+      ->orderBy($order, 'DESC')
+      ->range(0, $limit)
+      ->execute()
+      ->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteViews($id) {
+    return (bool) $this->connection
+      ->delete('node_counter')
+      ->condition('nid', $id)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetDayCount() {
+    $statistics_timestamp = $this->state->get('statistics.day_timestamp') ?: 0;
+    if (($this->getRequestTime() - $statistics_timestamp) >= 86400) {
+      $this->state->set('statistics.day_timestamp', $this->getRequestTime());
+      $this->connection->update('node_counter')
+        ->fields(['daycount' => 0])
+        ->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function maxTotalCount() {
+    $query = $this->connection->select('node_counter', 'nc');
+    $query->addExpression('MAX(totalcount)');
+    $max_total_count = (int)$query->execute()->fetchField();
+    return $max_total_count;
+  }
+
+  /**
+   * Get current request time.
+   *
+   * @return int
+   *   Unix timestamp for current server request time.
+   */
+  protected function getRequestTime() {
+    return $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
+  }
+
+}
diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
index 8f4384e2959208cd36b4a75f66d6ac7999d4f36d..8bd84b2b78ccc5041da94da38691cfa942e368f8 100644
--- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -4,8 +4,14 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\statistics\StatisticsStorageInterface;
 
 /**
  * Provides a 'Popular content' block.
@@ -15,7 +21,72 @@
  *   admin_label = @Translation("Popular content")
  * )
  */
-class StatisticsPopularBlock extends BlockBase {
+class StatisticsPopularBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The storage for statistics.
+   *
+   * @var \Drupal\statistics\StatisticsStorageInterface
+   */
+  protected $statisticsStorage;
+
+  /**
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs an StatisticsPopularBlock object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository service
+   * @param \Drupal\statistics\StatisticsStorageInterface $statistics_storage
+   *   The storage for statistics.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, StatisticsStorageInterface $statistics_storage, RendererInterface $renderer) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityRepository = $entity_repository;
+    $this->statisticsStorage = $statistics_storage;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('entity.repository'),
+      $container->get('statistics.storage.node'),
+      $container->get('renderer')
+    );
+  }
 
   /**
    * {@inheritdoc}
@@ -82,28 +153,64 @@ public function build() {
     $content = array();
 
     if ($this->configuration['top_day_num'] > 0) {
-      $result = statistics_title_list('daycount', $this->configuration['top_day_num']);
-      if ($result) {
-        $content['top_day'] = node_title_list($result, $this->t("Today's:"));
+      $nids = $this->statisticsStorage->fetchAll('daycount', $this->configuration['top_day_num']);
+      if ($nids) {
+        $content['top_day'] = $this->nodeTitleList($nids, $this->t("Today's:"));
         $content['top_day']['#suffix'] = '<br />';
       }
     }
 
     if ($this->configuration['top_all_num'] > 0) {
-      $result = statistics_title_list('totalcount', $this->configuration['top_all_num']);
-      if ($result) {
-        $content['top_all'] = node_title_list($result, $this->t('All time:'));
+      $nids = $this->statisticsStorage->fetchAll('totalcount', $this->configuration['top_all_num']);
+      if ($nids) {
+        $content['top_all'] = $this->nodeTitleList($nids, $this->t('All time:'));
         $content['top_all']['#suffix'] = '<br />';
       }
     }
 
     if ($this->configuration['top_last_num'] > 0) {
-      $result = statistics_title_list('timestamp', $this->configuration['top_last_num']);
-      $content['top_last'] = node_title_list($result, $this->t('Last viewed:'));
+      $nids = $this->statisticsStorage->fetchAll('timestamp', $this->configuration['top_last_num']);
+      $content['top_last'] = $this->nodeTitleList($nids, $this->t('Last viewed:'));
       $content['top_last']['#suffix'] = '<br />';
     }
 
     return $content;
   }
 
+  /**
+   * Generates the ordered array of node links for build().
+   *
+   * @param int[] $nids
+   *   An ordered array of node ids.
+   * @param string $title
+   *   The title for the list.
+   *
+   * @return array
+   *   A render array for the list.
+   */
+  protected function nodeTitleList(array $nids, $title) {
+    $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
+
+    $items = [];
+    foreach ($nids as $nid) {
+      $node = $this->entityRepository->getTranslationFromContext($nodes[$nid]);
+      $item = [
+        '#type' => 'link',
+        '#title' => $node->getTitle(),
+        '#url' => $node->urlInfo('canonical'),
+      ];
+      $this->renderer->addCacheableDependency($item, $node);
+      $items[] = $item;
+    }
+
+    return [
+      '#theme' => 'item_list__node',
+      '#items' => $items,
+      '#title' => $title,
+      '#cache' => [
+        'tags' => $this->entityTypeManager->getDefinition('node')->getListCacheTags(),
+      ],
+    ];
+  }
+
 }
diff --git a/core/modules/statistics/src/StatisticsStorageInterface.php b/core/modules/statistics/src/StatisticsStorageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..ccb51e44bc5614726efc30bb30c19331a19260d2
--- /dev/null
+++ b/core/modules/statistics/src/StatisticsStorageInterface.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\statistics;
+
+/**
+ * Provides an interface defining Statistics Storage.
+ *
+ * Stores the views per day, total views and timestamp of last view
+ * for entities.
+ */
+interface StatisticsStorageInterface {
+
+  /**
+   * Count a entity view.
+   *
+   * @param int $id
+   *   The ID of the entity to count.
+   *
+   * @return bool
+   *   TRUE if the entity view has been counted.
+   */
+  public function recordView($id);
+
+  /**
+   * Returns the number of times entities have been viewed.
+   *
+   * @param array $ids
+   *   An array of IDs of entities to fetch the views for.
+   *
+   * @return array \Drupal\statistics\StatisticsViewsResult
+   */
+  public function fetchViews($ids);
+
+  /**
+   * Returns the number of times a single entity has been viewed.
+   *
+   * @param int $id
+   *   The ID of the entity to fetch the views for.
+   *
+   * @return \Drupal\statistics\StatisticsViewsResult
+   */
+  public function fetchView($id);
+
+  /**
+   * Returns the number of times a entity has been viewed.
+   *
+   * @param string $order
+   *   The counter name to order by:
+   *   - 'totalcount' The total number of views.
+   *   - 'daycount' The number of views today.
+   *   - 'timestamp' The unix timestamp of the last view.
+   *
+   * @param int $limit
+   *   The number of entity IDs to return.
+   *
+   * @return array
+   *   An ordered array of entity IDs.
+   */
+  public function fetchAll($order = 'totalcount', $limit = 5);
+
+  /**
+   * Delete counts for a specific entity.
+   *
+   * @param int $id
+   *   The ID of the entity which views to delete.
+   *
+   * @return bool
+   *   TRUE if the entity views have been deleted.
+   */
+  public function deleteViews($id);
+
+  /**
+   * Reset the day counter for all entities once every day.
+   */
+  public function resetDayCount();
+
+  /**
+   * Returns the highest 'totalcount' value.
+   *
+   * @return int
+   *   The highest 'totalcount' value.
+   */
+  public function maxTotalCount();
+
+}
diff --git a/core/modules/statistics/src/StatisticsViewsResult.php b/core/modules/statistics/src/StatisticsViewsResult.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef0db9775e79783b9e34f9cd7f9cc487e3b7a569
--- /dev/null
+++ b/core/modules/statistics/src/StatisticsViewsResult.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\statistics;
+
+/**
+ * Value object for passing statistic results.
+ */
+class StatisticsViewsResult {
+
+  /**
+   * @var int
+   */
+  protected $totalCount;
+
+  /**
+   * @var int
+   */
+  protected $dayCount;
+
+  /**
+   * @var int
+   */
+  protected $timestamp;
+
+  public function __construct($total_count, $day_count, $timestamp) {
+    $this->totalCount = $total_count;
+    $this->dayCount = $day_count;
+    $this->timestamp = $timestamp;
+  }
+
+  /**
+   * Total number of times the entity has been viewed.
+   *
+   * @return int
+   */
+  public function getTotalCount() {
+    return $this->totalCount;
+  }
+
+
+  /**
+   * Total number of times the entity has been viewed "today".
+   *
+   * @return int
+   */
+  public function getDayCount() {
+    return $this->dayCount;
+  }
+
+
+  /**
+   * Timestamp of when the entity was last viewed.
+   *
+   * @return int
+   */
+  public function getTimestamp() {
+    return $this->timestamp;
+  }
+
+}
diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
index 9c0d26c5963bd081e9f98500b45ed54222c38939..0fe2e282027d6d384a0df62c8408e3400d915693 100644
--- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php
+++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\statistics\Tests;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+
 /**
  * Tests display of statistics report blocks.
  *
@@ -9,6 +12,8 @@
  */
 class StatisticsReportsTest extends StatisticsTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   /**
    * Tests the "popular content" block.
    */
@@ -30,7 +35,7 @@ function testPopularContentBlock() {
     $client->post($stats_path, array('headers' => $headers, 'body' => $post));
 
     // Configure and save the block.
-    $this->drupalPlaceBlock('statistics_popular_block', array(
+    $block = $this->drupalPlaceBlock('statistics_popular_block', array(
       'label' => 'Popular content',
       'top_day_num' => 3,
       'top_all_num' => 3,
@@ -44,9 +49,16 @@ function testPopularContentBlock() {
     $this->assertText('All time', 'Found the all time popular content.');
     $this->assertText('Last viewed', 'Found the last viewed popular content.');
 
-    // statistics.module doesn't use node entities, prevent the node language
-    // from being added to the options.
-    $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical', ['language' => NULL])), 'Found link to visited node.');
+    $tags = Cache::mergeTags($node->getCacheTags(), $block->getCacheTags());
+    $tags = Cache::mergeTags($tags, $this->blockingUser->getCacheTags());
+    $tags = Cache::mergeTags($tags, ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']);
+    $this->assertCacheTags($tags);
+    $contexts = Cache::mergeContexts($node->getCacheContexts(), $block->getCacheContexts());
+    $contexts = Cache::mergeContexts($contexts, ['url.query_args:_wrapper_format']);
+    $this->assertCacheContexts($contexts);
+
+    // Check if the node link is displayed.
+    $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical')), 'Found link to visited node.');
   }
 
 }
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index 5079e43cb322be4ea0bc5c2201c5111864428d3a..5419645ad625d8814ccb24daad42e1b68f45c1e5 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -52,9 +52,9 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
   if ($context['view_mode'] != 'rss') {
     $links['#cache']['contexts'][] = 'user.permissions';
     if (\Drupal::currentUser()->hasPermission('view post access counter')) {
-      $statistics = statistics_get($entity->id());
+      $statistics = \Drupal::service('statistics.storage.node')->fetchView($entity->id());
       if ($statistics) {
-        $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics['totalcount'], '1 view', '@count views');
+        $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics->getTotalCount(), '1 view', '@count views');
         $links['statistics'] = array(
           '#theme' => 'links__node__statistics',
           '#links' => $statistics_links,
@@ -70,18 +70,10 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
  * Implements hook_cron().
  */
 function statistics_cron() {
-  $statistics_timestamp = \Drupal::state()->get('statistics.day_timestamp') ?: 0;
-
-  if ((REQUEST_TIME - $statistics_timestamp) >= 86400) {
-    // Reset day counts.
-    db_update('node_counter')
-      ->fields(array('daycount' => 0))
-      ->execute();
-    \Drupal::state()->set('statistics.day_timestamp', REQUEST_TIME);
-  }
-
-  // Calculate the maximum of node views, for node search ranking.
-  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField()));
+  $storage = \Drupal::service('statistics.storage.node');
+  $storage->resetDayCount();
+  $max_total_count = $storage->maxTotalCount();
+  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count));
 }
 
 /**
@@ -123,26 +115,21 @@ function statistics_title_list($dbfield, $dbrows) {
   return FALSE;
 }
 
-
 /**
  * Retrieves a node's "view statistics".
  *
- * @param int $nid
- *   The node ID.
- *
- * @return array
- *   An associative array containing:
- *   - totalcount: Integer for the total number of times the node has been
- *     viewed.
- *   - daycount: Integer for the total number of times the node has been viewed
- *     "today". For the daycount to be reset, cron must be enabled.
- *   - timestamp: Integer for the timestamp of when the node was last viewed.
+ * @deprecated in Drupal 8.2.x, will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('statistics.storage.node')->fetchView($id).
  */
-function statistics_get($nid) {
-
-  if ($nid > 0) {
-    // Retrieve an array with both totalcount and daycount.
-    return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc();
+function statistics_get($id) {
+  if ($id > 0) {
+    /** @var \Drupal\statistics\StatisticsViewsResult $statistics */
+    $statistics = \Drupal::service('statistics.storage.node')->fetchView($id);
+    return [
+      'totalcount' => $statistics->getTotalCount(),
+      'daycount' => $statistics->getDayCount(),
+      'timestamp' => $statistics->getTimestamp(),
+    ];
   }
 }
 
@@ -151,9 +138,8 @@ function statistics_get($nid) {
  */
 function statistics_node_predelete(EntityInterface $node) {
   // Clean up statistics table when node is deleted.
-  db_delete('node_counter')
-    ->condition('nid', $node->id())
-    ->execute();
+  $id = $node->id();
+  return \Drupal::service('statistics.storage.node')->deleteViews($id);
 }
 
 /**
diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php
index a79af5f0f1c9d3df4582b1c09874aff32a156b93..a43509eaf84ca76a222cdf6c00af8921378ead11 100644
--- a/core/modules/statistics/statistics.php
+++ b/core/modules/statistics/statistics.php
@@ -14,8 +14,9 @@
 
 $kernel = DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloader, 'prod');
 $kernel->boot();
+$container = $kernel->getContainer();
 
-$views = $kernel->getContainer()
+$views = $container
   ->get('config.factory')
   ->get('statistics.settings')
   ->get('count_content_views');
@@ -23,15 +24,7 @@
 if ($views) {
   $nid = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT);
   if ($nid) {
-    \Drupal::database()->merge('node_counter')
-      ->key('nid', $nid)
-      ->fields(array(
-        'daycount' => 1,
-        'totalcount' => 1,
-        'timestamp' => REQUEST_TIME,
-      ))
-      ->expression('daycount', 'daycount + 1')
-      ->expression('totalcount', 'totalcount + 1')
-      ->execute();
+    $container->get('request_stack')->push(Request::createFromGlobals());
+    $container->get('statistics.storage.node')->recordView($nid);
   }
 }
diff --git a/core/modules/statistics/statistics.services.yml b/core/modules/statistics/statistics.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cf15573024f0aafdf7c33038448573054205f321
--- /dev/null
+++ b/core/modules/statistics/statistics.services.yml
@@ -0,0 +1,6 @@
+services:
+  statistics.storage.node:
+    class: Drupal\statistics\NodeStatisticsDatabaseStorage
+    arguments: ['@database', '@state', '@request_stack']
+    tags:
+      - { name: backend_overridable }