diff --git a/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php b/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php
new file mode 100644
index 0000000..8c0d7b1
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+/**
+ * Provides common functionality for computed field item lists.
+ *
+ * @see \Drupal\Core\Field\FieldItemListInterface
+ * @see \Drupal\Core\Field\FieldItemList
+ * @see \Drupal\Core\TypedData\ListInterface
+ * @see \Drupal\Core\TypedData\Plugin\DataType\ItemList
+ *
+ * @ingroup entity_api
+ */
+trait ComputedFieldItemListTrait {
+
+  /**
+   * Whether the values have already been computed or not.
+   *
+   * @var bool
+   */
+  protected $valueComputed = FALSE;
+
+  /**
+   * Computes the values for an item list.
+   */
+  abstract protected function computeValue();
+
+  /**
+   * Ensures that values are only computed once.
+   */
+  protected function doComputeValue() {
+    if ($this->valueComputed === FALSE) {
+      $this->computeValue();
+      $this->valueComputed = TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    $this->doComputeValue();
+    return parent::getValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($values, $notify = TRUE) {
+    parent::setValue($values, $notify);
+
+    // Make sure that subsequent getter calls do not try to compute the values
+    // again.
+    $this->valueComputed = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getString() {
+    $this->doComputeValue();
+    return parent::getString();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($index) {
+    if (!is_numeric($index)) {
+      throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
+    }
+
+    // Unlike the base implementation of
+    // \Drupal\Core\TypedData\ListInterface::get(), we do not add an empty item
+    // automatically because computed fields need to behave like non-computed
+    // ones. For example, calling isEmpty() on a computed field items list
+    // should return TRUE when the values were computed and the field is truly
+    // empty.
+    // @see \Drupal\Core\TypedData\Plugin\DataType\ItemList::get().
+    $this->doComputeValue();
+
+    return isset($this->list[$index]) ? $this->list[$index] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($index, $value) {
+    // Ensure that existing values are loaded when setting a value, this also
+    // ensures that it is possible to set a new value immediately after loading
+    // an entity.
+    $this->doComputeValue();
+    return parent::set($index, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function appendItem($value = NULL) {
+    $this->doComputeValue();
+    return parent::appendItem($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeItem($index) {
+    $this->doComputeValue();
+    return parent::removeItem($index);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $this->doComputeValue();
+    return parent::isEmpty();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter($callback) {
+    $this->doComputeValue();
+    return parent::filter($callback);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($offset) {
+    $this->doComputeValue();
+    return parent::offsetExists($offset);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    $this->doComputeValue();
+    return parent::getIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    $this->doComputeValue();
+    return parent::count();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyDefaultValue($notify = TRUE) {
+    // Default values do not make sense for computed fields. However, this
+    // method can be overridden if needed.
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filterEmptyItems() {
+    // If the values were never computed, for example when the field has not
+    // been interacted with during the life cycle of an entity, there is no need
+    // to compute them now.
+    if ($this->valueComputed === FALSE) {
+      return $this;
+    }
+
+    return parent::filterEmptyItems();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
index 864d33f..e3bff76 100644
--- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\entity_test\Plugin\Field;
 
+use Drupal\Core\Field\ComputedFieldItemListTrait;
 use Drupal\Core\Field\FieldItemList;
 
 /**
@@ -9,29 +10,20 @@
  */
 class ComputedTestFieldItemList extends FieldItemList {
 
+  use ComputedFieldItemListTrait;
+
   /**
    * Compute the list property from state.
    */
-  protected function computedListProperty() {
+  protected function computeValue() {
+    // Count the number of times this method has been executed during the
+    // lifecycle of an entity.
+    $execution_count = \Drupal::state()->get('computed_test_field_execution', 0);
+    \Drupal::state()->set('computed_test_field_execution', ++$execution_count);
+
     foreach (\Drupal::state()->get('entity_test_computed_field_item_list_value', []) as $delta => $item) {
       $this->list[$delta] = $this->createItem($delta, $item);
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function get($index) {
-    $this->computedListProperty();
-    return isset($this->list[$index]) ? $this->list[$index] : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIterator() {
-    $this->computedListProperty();
-    return parent::getIterator();
-  }
-
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
index 31b1f9e..8386508 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
@@ -739,13 +739,121 @@ public function testComputedProperties() {
   }
 
   /**
-   * Test computed fields.
+   * Tests all the interaction points of a computed field.
    */
   public function testComputedFields() {
+    $this->installEntitySchema('entity_test_computed_field');
+
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
+
+    // Check that the values are not computed unnecessarily during the life
+    // cycle of an entity when there the field is not interacted with directly.
+    \Drupal::state()->set('computed_test_field_execution', 0);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    $entity->name->value = $this->randomString();
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    $entity->save();
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getValue().
+    \Drupal::state()->set('computed_test_field_execution', 0);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame([['value' => 'foo computed']], $entity->computed_string_field->getValue());
+
+    // Check that the values are only computed once.
+    $this->assertSame(1, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::setValue(). This also
+    // checks that a subsequent getter does not try to re-compute the value.
+    \Drupal::state()->set('computed_test_field_execution', 0);
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->setValue([
+      ['value' => 'foo computed 1'],
+      ['value' => 'foo computed 2'],
+    ]);
+    $this->assertSame([['value' => 'foo computed 1'], ['value' => 'foo computed 2']], $entity->computed_string_field->getValue());
+
+    // Check that the values have not been computed when they were explicitly
+    // set.
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getString().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame('foo computed', $entity->computed_string_field->getString());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::get().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame('foo computed', $entity->computed_string_field->get(0)->value);
+    $this->assertEmpty($entity->computed_string_field->get(1));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::set().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->set(1, 'foo computed 1');
+    $this->assertSame('foo computed', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+    $entity->computed_string_field->set(0, 'foo computed 0');
+    $this->assertSame('foo computed 0', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::appendItem().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->appendItem('foo computed 1');
+    $this->assertSame('foo computed', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::removeItem().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->removeItem(0);
+    $this->assertTrue($entity->computed_string_field->isEmpty());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::isEmpty().
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', []);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertTrue($entity->computed_string_field->isEmpty());
+
     \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertFalse($entity->computed_string_field->isEmpty());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::filter().
+    $filter_callback = function ($item) {
+      return !$item->isEmpty();
+    };
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(1, $entity->computed_string_field);
+
+    // Add an empty item to the list and check that it is filtered out.
+    $entity->computed_string_field->appendItem();
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(1, $entity->computed_string_field);
+
+    $entity->computed_string_field->appendItem('foo computed 1');
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(2, $entity->computed_string_field);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::offsetExists().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertTrue($entity->computed_string_field->offsetExists(0));
+    $this->assertFalse($entity->computed_string_field->offsetExists(1));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getIterator().
+    $entity = EntityTestComputedField::create([]);
+    foreach ($entity->computed_string_field as $delta => $item) {
+      $this->assertSame('foo computed', $item->value);
+    }
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::count().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertCount(1, $entity->computed_string_field);
 
+    // Check that computed items are not auto-created when they have no values.
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', []);
     $entity = EntityTestComputedField::create([]);
-    $this->assertEquals($entity->computed_string_field->value, 'foo computed');
+    $this->assertCount(0, $entity->computed_string_field);
   }
 
   /**
