diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
index 1e55f29610fda7ca23f43796f2351f56bcbf59fa..12a6bbf734d3cfd8637df338cd92238a3ab5772b 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
@@ -54,8 +54,8 @@ function testNodeTokenReplacement() {
     $tests['[node:type]'] = 'article';
     $tests['[node:type-name]'] = 'Article';
     $tests['[node:title]'] = check_plain($node->getTitle());
-    $tests['[node:body]'] = text_sanitize($instance['settings']['text_processing'], $node->language()->id, $node->body[0]->getValue(), 'value');
-    $tests['[node:summary]'] = text_sanitize($instance['settings']['text_processing'], $node->language()->id, $node->body[0]->getValue(), 'summary');
+    $tests['[node:body]'] = $node->body->processed;
+    $tests['[node:summary]'] = $node->body->summary_processed;
     $tests['[node:langcode]'] = check_plain($node->language()->id);
     $tests['[node:url]'] = url('node/' . $node->id(), $url_options);
     $tests['[node:edit-url]'] = url('node/' . $node->id() . '/edit', $url_options);
@@ -97,7 +97,7 @@ function testNodeTokenReplacement() {
 
     // Generate and test sanitized token - use full body as expected value.
     $tests = array();
-    $tests['[node:summary]'] = text_sanitize($instance['settings']['text_processing'], $node->language()->id, $node->body[0]->getValue(), 'value');
+    $tests['[node:summary]'] = $node->body->processed;
 
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated for node without a summary.');
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index 92404974eded41c8bfa07db354a4925086b18066..397211f9fc0e29d4f51eab0aaa7cd9922ed0f23f 100644
--- a/core/modules/node/node.tokens.inc
+++ b/core/modules/node/node.tokens.inc
@@ -144,11 +144,11 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr
 
             // If the summary was requested and is not empty, use it.
             if ($name == 'summary' && !empty($item->summary)) {
-              $output = $sanitize ? text_sanitize($instance['settings']['text_processing'], $field_langcode, $item->getValue(), 'summary') : $item->summary;
+              $output = $sanitize ? $item->summary_processed : $item->summary;
             }
             // Attempt to provide a suitable version of the 'body' field.
             else {
-              $output = $sanitize ? text_sanitize($instance['settings']['text_processing'], $field_langcode, $item->getValue(), 'value') : $item->value;
+              $output = $sanitize ? $item->processed : $item->value;
               // A summary was requested.
               if ($name == 'summary') {
                 // Generate an optionally trimmed summary of the body field.
diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php
index 417d3541f3bebc14ea567c33067b40ff2d6680b0..54630fd8a1c281a69dcf3f83f49103715c736571 100644
--- a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php
+++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php
@@ -71,22 +71,34 @@ public function isEmpty() {
    * {@inheritdoc}
    */
   public function prepareCache() {
-    // Where possible, generate the sanitized version of each textual property
-    // (e.g., 'value', 'summary') within this field item early so that it is
-    // cached in the field cache. This avoids the need to look up the sanitized
-    // value in the filter cache separately.
+    // Where possible, generate the processed (sanitized) version of each
+    // textual property (e.g., 'value', 'summary') within this field item early
+    // so that it is cached in the field cache. This avoids the need to look up
+    // the sanitized value in the filter cache separately.
     $text_processing = $this->getFieldSetting('text_processing');
     if (!$text_processing || filter_format_allowcache($this->get('format')->getValue())) {
-      $itemBC = $this->getValue();
-      $langcode = $this->getParent()->getParent()->language()->id;
-      // The properties that need sanitizing are the ones that are the 'text
-      // source' of a TextProcessed computed property.
-      // @todo Clean up this mess by making the TextProcessed property type
-      //   support its own cache integration: https://drupal.org/node/2026339.
-      foreach ($this->getPropertyDefinitions() as $definition) {
+      foreach ($this->getPropertyDefinitions() as $property => $definition) {
         if (isset($definition['class']) && ($definition['class'] == '\Drupal\text\TextProcessed')) {
-          $source_property = $definition['settings']['text source'];
-          $this->set('safe_' . $source_property, text_sanitize($text_processing, $langcode, $itemBC, $source_property));
+          $this->get($property)->getValue();
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($property_name) {
+    // Notify the parent of changes.
+    if (isset($this->parent)) {
+      $this->parent->onChange($this->name);
+    }
+
+    // Unset processed properties that are affected by the change.
+    foreach ($this->getPropertyDefinitions() as $property => $definition) {
+      if (isset($definition['class']) && ($definition['class'] == '\Drupal\text\TextProcessed')) {
+        if ($property_name == 'format' || (isset($definition['settings']['text source']) && $definition['settings']['text source'] == $property_name)) {
+          $this->set($property, NULL, FALSE);
         }
       }
     }
diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php b/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
index dd0ee712b8a44a099b654ef301ae15bd7dd98d5d..a34da4318fce3e1e38021ba8403b214c4a76227b 100644
--- a/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
+++ b/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
@@ -38,11 +38,7 @@ public function viewElements(EntityInterface $entity, $langcode, FieldInterface
     $elements = array();
 
     foreach ($items as $delta => $item) {
-      // @todo Convert text_sanitize() to work on an NG $item. See
-      // https://drupal.org/node/2026339.
-      $itemBC = $item->getValue(TRUE);
-      $output = text_sanitize($this->getFieldSetting('text_processing'), $langcode, $itemBC, 'value');
-      $elements[$delta] = array('#markup' => $output);
+      $elements[$delta] = array('#markup' => $item->processed);
     }
 
     return $elements;
diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php b/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
index f2ca9e461fb9e51a9978076abe7e5cb15817b505..b59c73b82dd385408a14e16ae22ac217a03599ac 100644
--- a/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
+++ b/core/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
@@ -71,10 +71,10 @@ public function viewElements(EntityInterface $entity, $langcode, FieldInterface
     $text_processing = $this->getFieldSetting('text_processing');
     foreach ($items as $delta => $item) {
       if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) {
-        $output = text_sanitize($text_processing, $langcode, $item->getValue(TRUE), 'summary');
+        $output = $item->summary_processed;
       }
       else {
-        $output = text_sanitize($text_processing, $langcode, $item->getValue(TRUE), 'value');
+        $output = $item->processed;
         $output = text_summary($output, $text_processing ? $item->format : NULL, $this->getSetting('trim_length'));
       }
       $elements[$delta] = array('#markup' => $output);
diff --git a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..4cc22495d8c07b5cfdc60b5b5f1c73c39fc4f065
--- /dev/null
+++ b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\text\Tests\TextSummaryItemTest.
+ */
+
+namespace Drupal\text\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Entity\Field\FieldInterface;
+use Drupal\Core\Entity\Field\FieldItemInterface;
+use Drupal\field\Tests\FieldUnitTestBase;
+
+/**
+ * Tests for \Drupal\text\Plugin\field\field_type\TextWithSummaryItem.
+ */
+class TextWithSummaryItemTest extends FieldUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('filter');
+
+  /**
+   * Field entity.
+   *
+   * @var \Drupal\field\Entity\Field.
+   */
+  protected $field;
+
+  /**
+   * Field instance.
+   *
+   * @var \Drupal\field\Entity\FieldInstance
+   */
+  protected $instance;
+
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Text summary field item',
+      'description' => 'Tests using entity fields of the text summary field type.',
+      'group' => 'Field types',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+
+    // Create the necessary formats.
+    $this->installConfig(array('filter'));
+    entity_create('filter_format', array(
+      'format' => 'no_filters',
+      'filters' => array(),
+    ))->save();
+  }
+
+  /**
+   * Tests processed properties.
+   */
+  public function testCrudAndUpdate() {
+    $entity_type = 'entity_test';
+    $this->createField($entity_type);
+
+    // Create an entity with a summary and no text format.
+    $entity = entity_create($entity_type, array());
+    $entity->summary_field->value = $value = $this->randomName();
+    $entity->summary_field->summary = $summary = $this->randomName();
+    $entity->summary_field->format = NULL;
+    $entity->name->value = $this->randomName();
+    $entity->save();
+
+    $entity = entity_load($entity_type, $entity->id());
+    $this->assertTrue($entity->summary_field instanceof FieldInterface, 'Field implements interface.');
+    $this->assertTrue($entity->summary_field[0] instanceof FieldItemInterface, 'Field item implements interface.');
+    $this->assertEqual($entity->summary_field->value, $value);
+    $this->assertEqual($entity->summary_field->processed, $value);
+    $this->assertEqual($entity->summary_field->summary, $summary);
+    $this->assertEqual($entity->summary_field->summary_processed, $summary);
+    $this->assertNull($entity->summary_field->format);
+
+    // Enable text processing.
+    $this->instance->settings['text_processing'] = 1;
+    $this->instance->save();
+
+    // Re-load the entity.
+    $entity = entity_load($entity_type, $entity->id(), TRUE);
+
+    // Even if no format is given, if text processing is enabled, the default
+    // format is used.
+    $this->assertEqual($entity->summary_field->processed, "<p>$value</p>\n");
+    $this->assertEqual($entity->summary_field->summary_processed, "<p>$summary</p>\n");
+
+    // Change the format, this should update the processed properties.
+    $entity->summary_field->format = 'no_filters';
+    $this->assertEqual($entity->summary_field->processed, $value);
+    $this->assertEqual($entity->summary_field->summary_processed, $summary);
+  }
+
+  /**
+   * Tests that the processed values are cached.
+   */
+  function testProcessedCache() {
+    // Use an entity type that has caching enabled.
+    $entity_type = 'entity_test_rev';
+
+    $this->createField($entity_type);
+
+    // Create an entity with a summary and a text format.
+    $entity = entity_create($entity_type, array());
+    $entity->summary_field->value = $value = $this->randomName();
+    $entity->summary_field->summary = $summary = $this->randomName();
+    $entity->summary_field->format = 'plain_text';
+    $entity->name->value = $this->randomName();
+    $entity->save();
+
+    // Inject values into the cache to make sure that these are used as-is and
+    // not re-calculated.
+    $data = array(
+      'summary_field' => array(
+        Language::LANGCODE_DEFAULT => array(
+          0 => array(
+            'value' => $value,
+            'summary' => $summary,
+            'format' => 'plain_text',
+            'processed' => 'Cached processed value',
+            'summary_processed' => 'Cached summary processed value',
+          ),
+        ),
+      ),
+    );
+    cache('field')->set("field:$entity_type:" . $entity->id(), $data);
+
+    $entity = entity_load($entity_type, $entity->id());
+    $this->assertEqual($entity->summary_field->processed, 'Cached processed value');
+    $this->assertEqual($entity->summary_field->summary_processed, 'Cached summary processed value');
+
+    // Change the format, this should update the processed properties.
+    $entity->summary_field->format = 'no_filters';
+    $this->assertEqual($entity->summary_field->processed, $value);
+    $this->assertEqual($entity->summary_field->summary_processed, $summary);
+  }
+
+  /**
+   * Creates a text_with_summary field and field instance.
+   *
+   * @param string $entity_type
+   *   Entity type for which the field should be created.
+   */
+  protected function createField($entity_type) {
+    // Create a field .
+    $this->field = entity_create('field_entity', array(
+      'name' => 'summary_field',
+      'entity_type' => $entity_type,
+      'type' => 'text_with_summary',
+      'settings' => array(
+        'max_length' => 10,
+      )
+    ));
+    $this->field->save();
+    $this->instance = entity_create('field_instance', array(
+      'field_name' => $this->field->name,
+      'entity_type' => $entity_type,
+      'bundle' => $entity_type,
+      'settings' => array(
+        'text_processing' => 0,
+      )
+    ));
+    $this->instance->save();
+  }
+
+}
diff --git a/core/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/text/lib/Drupal/text/TextProcessed.php
index 352420de387fa74c8af189eb8720298b189f8f87..c7a9215cff39e8bb34c91cdf65cf0d067318c2b4 100644
--- a/core/modules/text/lib/Drupal/text/TextProcessed.php
+++ b/core/modules/text/lib/Drupal/text/TextProcessed.php
@@ -21,18 +21,11 @@
 class TextProcessed extends TypedData {
 
   /**
-   * The text property.
+   * Cached processed text.
    *
-   * @var \Drupal\Core\TypedData\TypedDataInterface
+   * @var string|null
    */
-  protected $text;
-
-  /**
-   * The text format property.
-   *
-   * @var \Drupal\Core\TypedData\TypedDataInterface
-   */
-  protected $format;
+  protected $processed = NULL;
 
   /**
    * Overrides TypedData::__construct().
@@ -45,47 +38,35 @@ public function __construct(array $definition, $name = NULL, TypedDataInterface
     }
   }
 
-  /**
-   * Overrides TypedData::setContext().
-   */
-  public function setContext($name = NULL, TypedDataInterface $parent = NULL) {
-    parent::setContext($name, $parent);
-    if (isset($parent)) {
-      $this->text = $parent->get($this->definition['settings']['text source']);
-      $this->format = $parent->get('format');
-    }
-  }
-
   /**
    * Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
    */
   public function getValue($langcode = NULL) {
-
-    if (!isset($this->text)) {
-      throw new InvalidArgumentException('Computed properties require context for computation.');
+    if ($this->processed !== NULL) {
+      return $this->processed;
     }
 
-    $field = $this->parent->getParent();
-    $entity = $field->getParent();
-    $instance = field_info_instance($entity->entityType(), $field->getName(), $entity->bundle());
-
-    if (!empty($instance['settings']['text_processing']) && $this->format->getValue()) {
-      return check_markup($this->text->getValue(), $this->format->getValue(), $entity->language()->id);
+    $item = $this->getParent();
+    $text = $item->{($this->definition['settings']['text source'])};
+    if ($item->getFieldDefinition()->getFieldSetting('text_processing')) {
+      $this->processed = check_markup($text, $item->format, $item->getLangcode());
     }
     else {
-      // If no format is available, still make sure to sanitize the text.
-      return check_plain($this->text->getValue());
+      // Escape all HTML and retain newlines.
+      // @see \Drupal\text\Plugin\field\formatter\TextPlainFormatter
+      $this->processed = nl2br(check_plain($text));
     }
+    return $this->processed;
   }
 
   /**
    * Implements \Drupal\Core\TypedData\TypedDataInterface::setValue().
    */
   public function setValue($value, $notify = TRUE) {
-    if (isset($value)) {
-      // @todo This is triggered from DatabaseStorageController::invokeFieldMethod()
-      // in the case of case of non-NG entity types.
-      // throw new ReadOnlyException('Unable to set a computed property.');
+    $this->processed = $value;
+    // Notify the parent of any changes.
+    if ($notify && isset($this->parent)) {
+      $this->parent->onChange($this->name);
     }
   }
 
diff --git a/core/modules/text/text.module b/core/modules/text/text.module
index 9014113eaea30869fe612a1b83e4128c84b5f954..ecc0767cc767e76b3c60fc4ec6a15a69011c2a7d 100644
--- a/core/modules/text/text.module
+++ b/core/modules/text/text.module
@@ -40,42 +40,6 @@ function text_help($path, $arg) {
   }
 }
 
-/**
- * Sanitizes the 'value' or 'summary' data of a text value.
- *
- * Depending on whether the field instance uses text processing, data is run
- * through check_plain() or check_markup().
- *
- * @param bool $text_processing
- *   Whether to process the text via check_markup().
- * @param string $langcode
- *  The language associated with $item.
- * @param array $item
- *   The field value to sanitize.
- * @param string $column
- *   The column to sanitize (either 'value' or 'summary').
- *
- * @return string
- *   The sanitized string.
- */
-function text_sanitize($text_processing, $langcode, $item, $column) {
-  if (isset($item["safe_$column"])) {
-    return $item["safe_$column"];
-  }
-
-  // Optimize by opting out for the trivial 'empty string' case.
-  if ($item[$column] == '') {
-    return '';
-  }
-
-  if ($text_processing) {
-    return check_markup($item[$column], $item['format'], $langcode);
-  }
-  // Escape all HTML and retain newlines.
-  // @see \Drupal\text\Plugin\field\formatter\TextPlainFormatter
-  return nl2br(check_plain($item[$column]));
-}
-
 /**
  * Generates a trimmed, formatted version of a text field value.
  *