diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php
index a56c1ef..70f6992 100644
--- a/core/includes/entity.api.php
+++ b/core/includes/entity.api.php
@@ -31,46 +31,6 @@ function hook_entity_info(&$entity_info) {
 }
 
 /**
- * Describe the view modes for entity types.
- *
- * View modes let entities be displayed differently depending on the context.
- * For instance, a node can be displayed differently on its own page ('full'
- * mode), on the home page or taxonomy listings ('teaser' mode), or in an RSS
- * feed ('rss' mode). Modules taking part in the display of the entity (notably
- * the Field API) can adjust their behavior depending on the requested view
- * mode. An additional 'default' view mode is available for all entity types.
- * This view mode is not intended for actual entity display, but holds default
- * display settings. For each available view mode, administrators can configure
- * whether it should use its own set of field display settings, or just
- * replicate the settings of the 'default' view mode, thus reducing the amount
- * of display configurations to keep track of.
- *
- * @return array
- *   An associative array of all entity view modes, keyed by the entity
- *   type name, and then the view mode name, with the following keys:
- *   - label: The human-readable name of the view mode.
- *   - custom_settings: A boolean specifying whether the view mode should by
- *     default use its own custom field display settings. If FALSE, entities
- *     displayed in this view mode will reuse the 'default' display settings
- *     by default (e.g. right after the module exposing the view mode is
- *     enabled), but administrators can later use the Field UI to apply custom
- *     display settings specific to the view mode.
- *
- * @see entity_get_view_modes()
- * @see hook_entity_view_mode_info_alter()
- */
-function hook_entity_view_mode_info() {
-  $view_modes['user']['full'] = array(
-    'label' => t('User account'),
-  );
-  $view_modes['user']['compact'] = array(
-    'label' => t('Compact'),
-    'custom_settings' => TRUE,
-  );
-  return $view_modes;
-}
-
-/**
  * Alter the view modes for entity types.
  *
  * @param array $view_modes
diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index 25d9915..f5b236a 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -121,13 +121,10 @@ function entity_get_view_modes($entity_type = NULL) {
       $view_modes = $cache->data;
     }
     else {
-      $view_modes = module_invoke_all('entity_view_mode_info');
-      foreach ($view_modes as $type => $entity_info) {
-        foreach ($entity_info as $view_mode => $view_mode_info) {
-          $view_modes[$type][$view_mode] += array(
-            'custom_settings' => FALSE,
-          );
-        }
+      $view_modes = array();
+      foreach (entity_load_multiple('view_mode') as $view_mode) {
+        list($view_mode_entity_type, $view_mode_name) = explode('.', $view_mode->id());
+        $view_modes[$view_mode_entity_type][$view_mode_name] = (array) $view_mode;
       }
       drupal_alter('entity_view_mode_info', $view_modes);
       cache()->set("entity_view_mode_info:$langcode", $view_modes, CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE));
diff --git a/core/modules/block/custom_block/config/view_mode.custom_block.full.yml b/core/modules/block/custom_block/config/view_mode.custom_block.full.yml
new file mode 100644
index 0000000..bed1d89
--- /dev/null
+++ b/core/modules/block/custom_block/config/view_mode.custom_block.full.yml
@@ -0,0 +1,5 @@
+id: custom_block.full
+label: Full
+custom_settings: '0'
+targetEntityType: custom_block
+locked: '1'
diff --git a/core/modules/block/custom_block/custom_block.module b/core/modules/block/custom_block/custom_block.module
index a3ce18c..75c7f50 100644
--- a/core/modules/block/custom_block/custom_block.module
+++ b/core/modules/block/custom_block/custom_block.module
@@ -199,16 +199,6 @@ function custom_block_entity_bundle_info() {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function custom_block_entity_view_mode_info() {
-  $view_modes['custom_block']['full'] = array(
-    'label' => t('Full'),
-  );
-  return $view_modes;
-}
-
-/**
  * Adds the default body field to a custom block type.
  *
  * @param string $block_type_id
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 26f324e..19982b2 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -261,17 +261,6 @@ function book_admin_paths() {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function book_entity_view_mode_info() {
-  // Add the 'Print' view mode for nodes.
-  $view_modes['node']['print'] = array(
-    'label' => t('Print'),
-  );
-  return $view_modes;
-}
-
-/**
  * Returns an array of all books.
  *
  * This list may be used for generating a list of all the books, or for building
diff --git a/core/modules/book/config/view_mode.node.print.yml b/core/modules/book/config/view_mode.node.print.yml
new file mode 100644
index 0000000..431abeb
--- /dev/null
+++ b/core/modules/book/config/view_mode.node.print.yml
@@ -0,0 +1,5 @@
+id: node.print
+label: Print
+custom_settings: '0'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 8e9400f..6649803 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -98,16 +98,6 @@ function comment_help($path, $arg) {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function comment_entity_view_mode_info() {
-  $view_modes['comment']['full'] = array(
-    'label' => t('Full comment'),
-  );
-  return $view_modes;
-}
-
-/**
  * Implements hook_entity_bundle_info().
  */
 function comment_entity_bundle_info() {
diff --git a/core/modules/comment/config/view_mode.comment.full.yml b/core/modules/comment/config/view_mode.comment.full.yml
new file mode 100644
index 0000000..892da5c
--- /dev/null
+++ b/core/modules/comment/config/view_mode.comment.full.yml
@@ -0,0 +1,5 @@
+id: comment.full
+label: Full comment
+custom_settings: '0'
+targetEntityType: comment
+locked: '1'
diff --git a/core/modules/entity/lib/Drupal/entity/EntityViewModeStorageController.php b/core/modules/entity/lib/Drupal/entity/EntityViewModeStorageController.php
new file mode 100644
index 0000000..495f0c9
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityViewModeStorageController.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity\EntityViewModeStorageController.
+ */
+
+namespace Drupal\entity;
+
+use Drupal\Core\Config\Entity\ConfigStorageController;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Defines the storage controller class for entity view modes.
+ */
+class EntityViewModeStorageController extends ConfigStorageController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function preSave(EntityInterface $view_mode) {
+    entity_info_cache_clear();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function preDelete($view_modes) {
+    entity_info_cache_clear();
+  }
+
+}
diff --git a/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityViewMode.php b/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityViewMode.php
new file mode 100644
index 0000000..bec7a90
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityViewMode.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\entity\Plugin\Core\Entity\EntityViewMode.
+ */
+
+namespace Drupal\entity\Plugin\Core\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Defines the view mode configuration entity class.
+ *
+ * View modes let entities be displayed differently depending on the context.
+ * For instance, a node can be displayed differently on its own page ('full'
+ * mode), on the home page or taxonomy listings ('teaser' mode), or in an RSS
+ * feed ('rss' mode). Modules taking part in the display of the entity (notably
+ * the Field API) can adjust their behavior depending on the requested view
+ * mode. An additional 'default' view mode is available for all entity types.
+ * This view mode is not intended for actual entity display, but holds default
+ * display settings. For each available view mode, administrators can configure
+ * whether it should use its own set of field display settings, or just
+ * replicate the settings of the 'default' view mode, thus reducing the amount
+ * of display configurations to keep track of.
+ *
+ * @see entity_get_view_modes()
+ * @see hook_entity_view_mode_info_alter()
+ *
+ * @EntityType(
+ *   id = "view_mode",
+ *   label = @Translation("View mode"),
+ *   module = "entity",
+ *   controller_class = "Drupal\entity\EntityViewModeStorageController",
+ *   config_prefix = "view_mode",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid"
+ *   }
+ * )
+ */
+class EntityViewMode extends ConfigEntityBase {
+
+  /**
+   * The ID of the view mode.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The UUID of the view mode.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The human-readable name of the view mode.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The entity type this view mode is used for.
+   *
+   * This is not to be confused with EntityViewMode::entityType which is
+   * inherited from Entity::entityType and equals 'view_mode' for any view mode
+   * entity.
+   *
+   * @var string
+   */
+  public $targetEntityType;
+
+  /**
+   * Whether or not this view mode has custom settings by default.
+   *
+   * If FALSE, entities displayed in this view mode will reuse the 'default'
+   * display settings by default (e.g. right after the module exposing the view
+   * mode is enabled), but administrators can later use the Field UI to apply
+   * custom display settings specific to the view mode.
+   *
+   * @var bool
+   */
+  public $custom_settings = FALSE;
+
+}
diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module
index a0f29e3..07c01c8 100644
--- a/core/modules/field_ui/field_ui.module
+++ b/core/modules/field_ui/field_ui.module
@@ -1,9 +1,12 @@
 <?php
+
 /**
  * @file
  * Allows administrators to attach custom fields to fieldable types.
  */
 
+use Drupal\entity\Plugin\Core\Entity\EntityViewMode;
+
 /**
  * Implements hook_help().
  */
@@ -429,3 +432,18 @@ function field_ui_library_info() {
 
   return $libraries;
 }
+
+/**
+ * Implements hook_view_mode_presave().
+ */
+function field_ui_view_mode_presave(EntityViewMode $view_mode) {
+  state()->set('menu_rebuild_needed', TRUE);
+}
+
+/**
+ * Implements hook_view_mode_delete().
+ */
+function field_ui_view_mode_delete(EntityViewMode $view_mode) {
+  state()->set('menu_rebuild_needed', TRUE);
+}
+
diff --git a/core/modules/file/config/view_mode.file.full.yml b/core/modules/file/config/view_mode.file.full.yml
new file mode 100644
index 0000000..f6c2096
--- /dev/null
+++ b/core/modules/file/config/view_mode.file.full.yml
@@ -0,0 +1,5 @@
+id: file.full
+label: File default
+custom_settings: '0'
+targetEntityType: file
+locked: '1'
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 69fcbf3..c447590 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -90,16 +90,6 @@ function file_element_info() {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function file_entity_view_mode_info() {
-  $view_modes['file']['full'] = array(
-    'label' => t('File default'),
-  );
-  return $view_modes;
-}
-
-/**
  * Loads file entities from the database.
  *
  * @param array $fids
diff --git a/core/modules/node/config/view_mode.node.full.yml b/core/modules/node/config/view_mode.node.full.yml
new file mode 100644
index 0000000..6af61fc
--- /dev/null
+++ b/core/modules/node/config/view_mode.node.full.yml
@@ -0,0 +1,5 @@
+id: node.full
+label: Full content
+custom_settings: '0'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/node/config/view_mode.node.rss.yml b/core/modules/node/config/view_mode.node.rss.yml
new file mode 100644
index 0000000..755f92c
--- /dev/null
+++ b/core/modules/node/config/view_mode.node.rss.yml
@@ -0,0 +1,5 @@
+id: node.rss
+label: RSS
+custom_settings: '0'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/node/config/view_mode.node.teaser.yml b/core/modules/node/config/view_mode.node.teaser.yml
new file mode 100644
index 0000000..0400376
--- /dev/null
+++ b/core/modules/node/config/view_mode.node.teaser.yml
@@ -0,0 +1,5 @@
+id: node.teaser
+label: Teaser
+custom_settings: '1'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 9bd3087..0d1145c 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -191,33 +191,6 @@ function node_entity_info_alter(&$info) {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function node_entity_view_mode_info() {
-  $view_modes['node']['full'] = array(
-    'label' => t('Full content'),
-  );
-  $view_modes['node']['teaser'] = array(
-    'label' => t('Teaser'),
-    'custom_settings' => TRUE,
-  );
-  $view_modes['node']['rss'] = array(
-    'label' => t('RSS'),
-  );
-  // Search integration is provided by node.module, so search-related
-  // view modes for nodes are defined here and not in search.module.
-  if (module_exists('search')) {
-    $view_modes['node']['search_index'] = array(
-      'label' => t('Search index'),
-    );
-    $view_modes['node']['search_result'] = array(
-      'label' => t('Search result'),
-    );
-  }
-  return $view_modes;
-}
-
-/**
  * Implements hook_entity_bundle_info().
  */
 function node_entity_bundle_info() {
diff --git a/core/modules/search/config/view_mode.node.search_index.yml b/core/modules/search/config/view_mode.node.search_index.yml
new file mode 100644
index 0000000..85bd27f
--- /dev/null
+++ b/core/modules/search/config/view_mode.node.search_index.yml
@@ -0,0 +1,5 @@
+id: node.search_index
+label: Search index
+custom_settings: '0'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/search/config/view_mode.node.search_result.yml b/core/modules/search/config/view_mode.node.search_result.yml
new file mode 100644
index 0000000..f023ad7
--- /dev/null
+++ b/core/modules/search/config/view_mode.node.search_result.yml
@@ -0,0 +1,5 @@
+id: node.search_result
+label: Search result
+custom_settings: '0'
+targetEntityType: node
+locked: '1'
diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
index 9fe3328..33f7903 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
@@ -99,6 +99,13 @@ public function testFilledStandardUpgrade() {
     $this->assertEqual($blog_type->module, 'node', "Content type 'blog' has been reassigned from the blog module to the node module.");
     $this->assertEqual($blog_type->base, 'node_content', "The base string used to construct callbacks corresponding to content type 'Blog' has been reassigned to 'node_content'.");
 
+    // Each entity type has a 'full' view mode, ensure it was migrated.
+    $all_view_modes = entity_get_view_modes();
+    $this->assertTrue(!empty($all_view_modes), 'The view modes have been migrated.');
+    foreach ($all_view_modes as $entity_view_modes) {
+      $this->assertTrue(isset($entity_view_modes['full']));
+    }
+
     // Check that user data has been migrated correctly.
     $query = db_query('SELECT * FROM {users_data}');
 
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 493db9b..ff8702c 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2179,6 +2179,57 @@ function system_update_8053() {
 }
 
 /**
+ * Moves entity view modes to config.
+ *
+ * @ingroup config_upgrade
+ */
+function system_update_8054() {
+  // We cannot call entity_get_info() in an update hook, so we hardcode the view
+  // modes. Only the node entity type's teaser view mode is set to custom by
+  // default, we check specifically for that below. The only way to add custom
+  // view modes in Drupal 7 was hook_entity_info_alter(), which still works in
+  // Drupal 8.
+  $entity_view_modes = array(
+    'node' => array(
+      'full' => 'Full content',
+      'teaser' => 'Teaser',
+      'rss' => 'RSS',
+      'search_index' => 'Search index',
+      'search_result' => 'Search result',
+      'print' => 'Print',
+    ),
+    'file' => array(
+      'full' => 'File default',
+    ),
+    'comment' => array(
+      'full' => 'Full comment',
+    ),
+    'user' => array(
+      'full' => 'User account',
+    ),
+    'taxonomy_term' => array(
+      'full' => 'Taxonomy term page',
+    ),
+    'taxonomy_vocabulary' => array(
+      'full' => 'Taxonomy vocabulary',
+    ),
+  );
+
+  foreach ($entity_view_modes as $entity_type => $view_modes) {
+    foreach ($view_modes as $key => $name) {
+      $custom_settings = ($key == 'teaser');
+      config("view_mode.$entity_type.$key")
+        ->set('id', "$entity_type.$key")
+        ->set('label', $name)
+        ->set('targetEntityType', $entity_type)
+        ->set('locked', TRUE)
+        ->set('custom_settings', $custom_settings)
+        ->save();
+    }
+  }
+}
+
+/**
  * @} End of "defgroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.full.yml b/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.full.yml
new file mode 100644
index 0000000..874aebe
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.full.yml
@@ -0,0 +1,5 @@
+id: entity_test_render.full
+label: Full
+custom_settings: '0'
+targetEntityType: entity_test_render
+locked: '1'
diff --git a/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.test.yml b/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.test.yml
new file mode 100644
index 0000000..724255f
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/config/view_mode.entity_test_render.test.yml
@@ -0,0 +1,5 @@
+id: entity_test_render.test
+label: Test
+custom_settings: '0'
+targetEntityType: entity_test_render
+locked: '1'
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 80f1b39..d9ca4cb 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -270,20 +270,6 @@ function entity_test_label_callback($entity_type, $entity, $langcode = NULL) {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function entity_test_entity_view_mode_info() {
-  $view_modes['entity_test_render']['full'] = array(
-    'label' => t('Full'),
-  );
-  $view_modes['entity_test_render']['test'] = array(
-    'label' => t('Test'),
-  );
-
-  return $view_modes;
-}
-
-/**
  * Implements hook_entity_field_access().
  *
  * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
diff --git a/core/modules/taxonomy/config/view_mode.taxonomy_term.full.yml b/core/modules/taxonomy/config/view_mode.taxonomy_term.full.yml
new file mode 100644
index 0000000..63c16d7
--- /dev/null
+++ b/core/modules/taxonomy/config/view_mode.taxonomy_term.full.yml
@@ -0,0 +1,5 @@
+id: taxonomy_term.full
+label: Taxonomy term page
+custom_settings: '0'
+targetEntityType: taxonomy_term
+locked: '1'
diff --git a/core/modules/taxonomy/config/view_mode.taxonomy_vocabulary.full.yml b/core/modules/taxonomy/config/view_mode.taxonomy_vocabulary.full.yml
new file mode 100644
index 0000000..69d2386
--- /dev/null
+++ b/core/modules/taxonomy/config/view_mode.taxonomy_vocabulary.full.yml
@@ -0,0 +1,5 @@
+id: vocabulary.full
+label: Taxonomy vocabulary
+custom_settings: '0'
+targetEntityType: taxonomy_vocabulary
+locked: '1'
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 43d2ac8..42e3ded 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -106,19 +106,6 @@ function taxonomy_permission() {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function taxonomy_entity_view_mode_info() {
-  $view_modes['taxonomy_term']['full'] = array(
-    'label' => t('Taxonomy term page'),
-  );
-  $view_modes['taxonomy_vocabulary']['full'] = array(
-    'label' => t('Taxonomy vocabulary'),
-  );
-  return $view_modes;
-}
-
-/**
  * Implements hook_entity_bundle_info().
  */
 function taxonomy_entity_bundle_info() {
diff --git a/core/modules/user/config/view_mode.user.full.yml b/core/modules/user/config/view_mode.user.full.yml
new file mode 100644
index 0000000..d3880a1
--- /dev/null
+++ b/core/modules/user/config/view_mode.user.full.yml
@@ -0,0 +1,5 @@
+id: user.full
+label: User account
+custom_settings: '0'
+targetEntityType: user
+locked: '1'
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index d41206f..eb3e092 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -125,20 +125,6 @@ function user_page_build(&$page) {
 }
 
 /**
- * Implements hook_entity_view_mode_info().
- */
-function user_entity_view_mode_info() {
-  $view_modes['user']['full'] = array(
-    'label' => t('User account'),
-  );
-  $view_modes['user']['compact'] = array(
-    'label' => t('Compact'),
-    'custom_settings' => TRUE,
-  );
-  return $view_modes;
-}
-
-/**
  * Implements hook_entity_bundle_info().
  */
 function user_entity_bundle_info() {
