diff --git a/includes/common.inc b/includes/common.inc
index b5686cab7ebd94bbf80f8b97d98544d2a1ed7bf8..ce21e4fa49a57bcd79033d4247a2842f8e32b0ee 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2960,8 +2960,9 @@ function drupal_add_js($data = NULL, $options = NULL) {
           'preprocess' => TRUE,
         ),
       );
-      // jQuery itself is registered as a library.
+      // Register all required libraries.
       drupal_add_library('system', 'jquery');
+      drupal_add_library('system', 'once');
     }
 
     switch ($options['type']) {
diff --git a/misc/autocomplete.js b/misc/autocomplete.js
index 87e724d4671e8a080d57fd1431c92ea26af67435..489e2fb1b5618a46181b73adcb5165c78366fbb1 100644
--- a/misc/autocomplete.js
+++ b/misc/autocomplete.js
@@ -7,7 +7,7 @@
 Drupal.behaviors.autocomplete = {
   attach: function (context, settings) {
     var acdb = [];
-    $('input.autocomplete:not(.autocomplete-processed)', context).each(function () {
+    $('input.autocomplete', context).once('autocomplete', function () {
       var uri = this.value;
       if (!acdb[uri]) {
         acdb[uri] = new Drupal.ACDB(uri);
@@ -16,7 +16,6 @@ Drupal.behaviors.autocomplete = {
         .attr('autocomplete', 'OFF')[0];
       $(input.form).submit(Drupal.autocompleteSubmit);
       new Drupal.jsAC(input, acdb[uri]);
-      $(this).addClass('autocomplete-processed');
     });
   }
 };
diff --git a/misc/batch.js b/misc/batch.js
index 8e53c210af652b8f44fce0c25f42297c08c7ca8b..d0a32f2a1210f3aa97769d9e1a131b88b8f1ee90 100644
--- a/misc/batch.js
+++ b/misc/batch.js
@@ -6,11 +6,7 @@
  */
 Drupal.behaviors.batch = {
   attach: function (context, settings) {
-    // This behavior attaches by ID, so is only valid once on a page.
-    if ($('#progress.batch-processed').size()) {
-      return;
-    }
-    $('#progress', context).addClass('batch-processed').each(function () {
+    $('#progress', context).once('batch', function () {
       var holder = $(this);
 
       // Success: redirect to the summary.
diff --git a/misc/collapse.js b/misc/collapse.js
index c4fac9be3ce539d5c0ca1a0dfea9f9b0ffa10114..741f4b0bae382054d36fa14d88f70fa32d535a9b 100644
--- a/misc/collapse.js
+++ b/misc/collapse.js
@@ -53,7 +53,7 @@ Drupal.collapseScrollIntoView = function (node) {
 
 Drupal.behaviors.collapse = {
   attach: function (context, settings) {
-    $('fieldset.collapsible > legend:not(.collapse-processed)', context).each(function () {
+    $('fieldset.collapsible > legend', context).once('collapse', function () {
       var fieldset = $(this.parentNode);
       // Expand if there are errors inside
       if ($('input.error, textarea.error, select.error', fieldset).size() > 0) {
@@ -81,9 +81,10 @@ Drupal.behaviors.collapse = {
           return false;
         }))
         .append(summary)
-        .after($('<div class="fieldset-wrapper"></div>')
-          .append(fieldset.children(':not(legend):not(.action)'))
-        ).addClass('collapse-processed');
+        .after(
+          $('<div class="fieldset-wrapper"></div>')
+            .append(fieldset.children(':not(legend):not(.action)'))
+        );
     });
   }
 };
diff --git a/misc/drupal.js b/misc/drupal.js
index dbd057bb914db8f55af6d44218295ad86ecae86a..ea688a499a3843ff0396d6eb7a83fe4fdcf04044 100644
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -12,7 +12,6 @@ if ($ === undefined) {
   };
 }
 
-
 (function ($) {
 
 /**
@@ -38,10 +37,15 @@ if ($ === undefined) {
  * loaded, feeding in an element to be processed, in order to attach all
  * behaviors to the new content.
  *
- * Behaviors should use a class in the form behaviorName-processed to ensure
- * the behavior is attached only once to a given element. (Doing so enables
- * the reprocessing of given elements, which may be needed on occasion despite
- * the ability to limit behavior attachment to a particular element.)
+ * Behaviors should use
+ * @code
+ *   $(selector).once('behavior-name', function () {
+ *     ...
+ *   });
+ * @endcode
+ * to ensure the behavior is attached only once to a given element. (Doing so
+ * enables the reprocessing of given elements, which may be needed on occasion
+ * despite the ability to limit behavior attachment to a particular element.)
  *
  * @param context
  *   An element to attach behaviors to. If none is given, the document element
diff --git a/misc/form.js b/misc/form.js
index a59451760ce2b20bad5906027325dd42af43aeee..ca572cf38fcbc0c676c070ebafc3d8196822a0c1 100644
--- a/misc/form.js
+++ b/misc/form.js
@@ -61,8 +61,7 @@ Drupal.behaviors.formUpdated = {
 Drupal.behaviors.multiselectSelector = {
   attach: function (context, settings) {
     // Automatically selects the right radio button in a multiselect control.
-    $('.multiselect select:not(.multiselectSelector-processed)', context)
-      .addClass('multiselectSelector-processed').change(function () {
+    $('.multiselect select', context).once('multiselect').change(function () {
         $('.multiselect input:radio[value="' + this.id.substr(5) + '"]')
           .attr('checked', true);
     });
@@ -75,8 +74,7 @@ Drupal.behaviors.multiselectSelector = {
  */
 Drupal.behaviors.filterGuidelines = {
   attach: function (context) {
-    $('.filter-guidelines:not(.filter-guidelines-processed)', context)
-      .addClass('filter-guidelines-processed')
+    $('.filter-guidelines', context).once('filter-guidelines')
       .find('label').hide()
       .parents('.filter-wrapper').find('select.filter-list')
       .bind('change', function () {
diff --git a/misc/tabledrag.js b/misc/tabledrag.js
index db40f2437ef25b83a4e8f3796a78ed4ffe7b15ae..0c7acbc751ee834459748c795471a8c92b534006 100644
--- a/misc/tabledrag.js
+++ b/misc/tabledrag.js
@@ -15,17 +15,11 @@
 Drupal.behaviors.tableDrag = {
   attach: function (context, settings) {
     for (var base in settings.tableDrag) {
-      if (!$('#' + base + '.tabledrag-processed', context).size()) {
-        var tableSettings = settings.tableDrag[base];
-
-        $('#' + base).filter(':not(.tabledrag-processed)').each(function () {
-          // Create the new tableDrag instance. Save in the Drupal variable
-          // to allow other scripts access to the object.
-          Drupal.tableDrag[base] = new Drupal.tableDrag(this, tableSettings);
-        });
-
-        $('#' + base).addClass('tabledrag-processed');
-      }
+      $('#' + base, context).once('tabledrag', function () {
+        // Create the new tableDrag instance. Save in the Drupal variable
+        // to allow other scripts access to the object.
+        Drupal.tableDrag[base] = new Drupal.tableDrag(this, settings.tableDrag[base]);
+      });
     }
   }
 };
diff --git a/misc/tableheader.js b/misc/tableheader.js
index a49dd2d401646e8ca381edd1b57b0ccd6e451405..52f6b6d33e3909247560b5394bccd0b107d42da3 100644
--- a/misc/tableheader.js
+++ b/misc/tableheader.js
@@ -17,7 +17,7 @@ Drupal.behaviors.tableHeader = {
     // Keep track of all cloned table headers.
     var headers = [];
 
-    $('table.sticky-enabled thead:not(.tableHeader-processed)', context).each(function () {
+    $('table.sticky-enabled thead', context).once('tableheader', function () {
       // Clone thead so it inherits original jQuery properties.
       var headerClone = $(this).clone(true).insertBefore(this.parentNode).wrap('<table class="sticky-header"></table>').parent().css({
         position: 'fixed',
@@ -34,7 +34,6 @@ Drupal.behaviors.tableHeader = {
       tracker(headerClone);
 
       $(table).addClass('sticky-table');
-      $(this).addClass('tableHeader-processed');
     });
 
     // Define the anchor holding var.
@@ -81,11 +80,10 @@ Drupal.behaviors.tableHeader = {
 
     // Only attach to scrollbars once, even if Drupal.attachBehaviors is called
     //  multiple times.
-    if (!$('body').hasClass('tableHeader-processed')) {
-      $('body').addClass('tableHeader-processed');
+    $('body').once(function () {
       $(window).scroll(Drupal.tableHeaderDoScroll);
       $(document.documentElement).scroll(Drupal.tableHeaderDoScroll);
-    }
+    });
 
     // Track scrolling.
     Drupal.tableHeaderOnScroll = function () {
diff --git a/misc/tableselect.js b/misc/tableselect.js
index 332a1d9e5566582db3cc9a96c8ab268876ff3e60..cba0c57a5fba82b173389849d18e81bc67344549 100644
--- a/misc/tableselect.js
+++ b/misc/tableselect.js
@@ -3,7 +3,7 @@
 
 Drupal.behaviors.tableSelect = {
   attach: function (context, settings) {
-    $('form table:has(th.select-all):not(.tableSelect-processed)', context).each(Drupal.tableSelect);
+    $('form table:has(th.select-all)', context).once('table-select', Drupal.tableSelect);
   }
 };
 
@@ -56,7 +56,6 @@ Drupal.tableSelect = function () {
     // Keep track of the last checked checkbox.
     lastChecked = e.target;
   });
-  $(this).addClass('tableSelect-processed');
 };
 
 Drupal.tableSelectRange = function (from, to, state) {
diff --git a/misc/textarea.js b/misc/textarea.js
index cec9abfff71530602d37986959b76ead6d2e99f7..5938b9e4db69d4765ecc83c5c0b4d4eacac96f5d 100644
--- a/misc/textarea.js
+++ b/misc/textarea.js
@@ -3,20 +3,16 @@
 
 Drupal.behaviors.textarea = {
   attach: function (context, settings) {
-    $('textarea.resizable:not(.textarea-processed)', context).each(function () {
-      // Avoid non-processed teasers.
-      if ($(this).is(('textarea.teaser:not(.teaser-processed)'))) {
-        return false;
-      }
-      var textarea = $(this).addClass('textarea-processed'), staticOffset = null;
-
+    $('textarea.resizable', context).once('textarea', function () {
       // When wrapping the text area, work around an IE margin bug. See:
       // http://jaspan.com/ie-inherited-margin-bug-form-elements-and-haslayout
-      $(this).wrap('<div class="resizable-textarea"><span></span></div>')
-        .parent().append($('<div class="grippie"></div>').mousedown(startDrag));
+      var staticOffset = null;
+      var textarea = $(this).wrap('<div class="resizable-textarea"><span></span></div>');
+      var grippie = $('<div class="grippie"></div>').mousedown(startDrag);
 
-      var grippie = $('div.grippie', $(this).parent())[0];
-      grippie.style.marginRight = (grippie.offsetWidth - $(this)[0].offsetWidth) + 'px';
+      grippie
+        .insertAfter(textarea)
+        .css('margin-right', grippie.width() - textarea.width());
 
       function startDrag(e) {
         staticOffset = textarea.height() - e.pageY;
diff --git a/misc/timezone.js b/misc/timezone.js
index a1e689c92ffbd548e130e0f12ee626f4c2212e78..b708875f5bf22a37b33d42669b5c78b52779ff79 100644
--- a/misc/timezone.js
+++ b/misc/timezone.js
@@ -6,7 +6,7 @@
  */
 Drupal.behaviors.setTimezone = {
   attach: function (context, settings) {
-    $('select.timezone-detect:not(.timezone-processed)', context).addClass('timezone-processed').each(function () {
+    $('select.timezone-detect', context).once('timezone', function () {
       var dateString = Date();
       // In some client environments, date strings include a time zone
       // abbreviation, between 3 and 5 letters enclosed in parentheses,
diff --git a/misc/vertical-tabs.js b/misc/vertical-tabs.js
index 7fb15549f5ae7552517faf3600289ba999a343db..32c4ee697307aa2765807a9edffc2656168ab3af 100644
--- a/misc/vertical-tabs.js
+++ b/misc/vertical-tabs.js
@@ -15,7 +15,7 @@
  */
 Drupal.behaviors.verticalTabs = {
   attach: function (context) {
-    $('.vertical-tabs-panes:not(.vertical-tabs-processed)', context).each(function () {
+    $('.vertical-tabs-panes', context).once('vertical-tabs', function () {
       var focusID = $(':hidden.vertical-tabs-active-tab', this).val();
       var focus;
       // Create the tab column.
@@ -42,7 +42,7 @@ Drupal.behaviors.verticalTabs = {
         focus = $('> .vertical-tabs-pane:first', this);
       }
       focus.data('verticalTab').focus();
-    }).addClass('vertical-tabs-processed');
+    });
   }
 };
 
diff --git a/modules/block/block.js b/modules/block/block.js
index 34c04d2460c6902945b95aeff472f06947c323cf..e83f6ea04e8be956438fb0822aa6b194a9ef0806 100644
--- a/modules/block/block.js
+++ b/modules/block/block.js
@@ -51,7 +51,7 @@ Drupal.behaviors.blockDrag = {
     };
 
     // Add the behavior to each region select list.
-    $('select.block-region-select:not(.blockregionselect-processed)', context).each(function () {
+    $('select.block-region-select', context).once('block-region-select', function () {
       $(this).change(function (event) {
         // Make our new row and select field.
         var row = $(this).parents('tr:first');
@@ -82,7 +82,6 @@ Drupal.behaviors.blockDrag = {
         // Remove focus from selectbox.
         select.get(0).blur();
       });
-      $(this).addClass('blockregionselect-processed');
     });
 
     var checkEmptyRegions = function (table, rowObject) {
diff --git a/modules/color/color.js b/modules/color/color.js
index c286c40a9ea4791ff5b4827c70cdd86e00123c2a..a834f11edac7e47313094058cc011f1f2053c0c7 100644
--- a/modules/color/color.js
+++ b/modules/color/color.js
@@ -4,10 +4,10 @@
 Drupal.behaviors.color = {
   attach: function (context, settings) {
     // This behavior attaches by ID, so is only valid once on a page.
-    if ($('#color_scheme_form .color-form.color-processed').size()) {
+    var form = $('#system-theme-settings .color-form', context).once('color');
+    if (form.length == 0) {
       return;
     }
-    var form = $('#system-theme-settings .color-form', context);
     var inputs = [];
     var hooks = [];
     var locks = [];
@@ -24,9 +24,7 @@ Drupal.behaviors.color = {
     }
 
     // Build a preview.
-    $('#preview:not(.color-processed)')
-      .append('<div id="gradient"></div>')
-      .addClass('color-processed');
+    $('#preview').once('color').append('<div id="gradient"></div>');
     var gradient = $('#preview #gradient');
     var h = parseInt(gradient.css('height')) / 10;
     for (i = 0; i < h; ++i) {
diff --git a/modules/comment/comment.js b/modules/comment/comment.js
index bdd1c3b952a799fc034e06d0876e5fba7ecdb9f6..58f00dc6b526731774e35855874bf5e2eba35808 100644
--- a/modules/comment/comment.js
+++ b/modules/comment/comment.js
@@ -6,9 +6,7 @@ Drupal.behaviors.comment = {
     $.each(['name', 'homepage', 'mail'], function () {
       var cookie = Drupal.comment.getCookie('comment_info_' + this);
       if (cookie) {
-        $('#comment-form input[name=' + this + ']:not(.comment-processed)', context)
-          .val(cookie)
-          .addClass('comment-processed');
+        $('#comment-form input[name=' + this + ']', context).once('comment').val(cookie);
       }
     });
   }
diff --git a/modules/system/system.js b/modules/system/system.js
index 11aa8b4b8cbaf2469b97f9f4be758f553b84bd15..4da80811d9dfed31df0db1ca820842e03efff460 100644
--- a/modules/system/system.js
+++ b/modules/system/system.js
@@ -32,7 +32,7 @@ Drupal.behaviors.cleanURLsSettingsCheck = {
     // This behavior attaches by ID, so is only valid once on a page.
     // Also skip if we are on an install page, as Drupal.cleanURLsInstallCheck will handle
     // the processing.
-    if (!($('#edit-clean-url').size()) || $('.clean-url-processed, #edit-clean-url.install').size()) {
+    if (!($('#edit-clean-url').length) || $('#edit-clean-url.install').once('clean-url').length) {
       return;
     }
     var url = settings.basePath + 'admin/config/search/clean-urls/check';
@@ -44,7 +44,6 @@ Drupal.behaviors.cleanURLsSettingsCheck = {
         location = settings.basePath +"admin/config/search/clean-urls";
       }
     });
-    $('#clean-url').addClass('clean-url-processed');
   }
 };
 
@@ -68,7 +67,6 @@ Drupal.cleanURLsInstallCheck = function () {
       $('#edit-clean-url').attr('value', 1);
     }
   });
-  $('#edit-clean-url').addClass('clean-url-processed');
 };
 
 /**
@@ -79,21 +77,17 @@ Drupal.cleanURLsInstallCheck = function () {
 Drupal.behaviors.copyFieldValue = {
   attach: function (context, settings) {
     for (var sourceId in settings.copyFieldValue) {
-      // Get the list of target fields.
-      targetIds = settings.copyFieldValue[sourceId];
-      if (!$('#'+ sourceId + '.copy-field-values-processed', context).size()) {
+      $('#' + sourceId, context).once('copy-field-values').bind('blur', function () {
+        // Get the list of target fields.
+        var targetIds = settings.copyFieldValue[sourceId];
         // Add the behavior to update target fields on blur of the primary field.
-        sourceField = $('#' + sourceId);
-        sourceField.bind('blur', function () {
-          for (var delta in targetIds) {
-            var targetField = $('#'+ targetIds[delta]);
-            if (targetField.val() == '') {
-              targetField.val(this.value);
-            }
+        for (var delta in targetIds) {
+          var targetField = $('#' + targetIds[delta]);
+          if (targetField.val() == '') {
+            targetField.val(this.value);
           }
-        });
-        sourceField.addClass('copy-field-values-processed');
-      }
+        }
+      });
     }
   }
 };
@@ -104,12 +98,12 @@ Drupal.behaviors.copyFieldValue = {
 Drupal.behaviors.dateTime = {
   attach: function (context, settings) {
     // Show/hide custom format depending on the select's value.
-    $('select.date-format:not(.date-time-processed)', context).change(function () {
-      $(this).addClass('date-time-processed').parents('div.date-container').children('div.custom-container')[$(this).val() == 'custom' ? 'show' : 'hide']();
+    $('select.date-format', context).once('date-time').change(function () {
+      $(this).parents('div.date-container').children('div.custom-container')[$(this).val() == 'custom' ? 'show' : 'hide']();
     });
 
     // Attach keyup handler to custom format inputs.
-    $('input.custom-format:not(.date-time-processed)', context).addClass('date-time-processed').keyup(function () {
+    $('input.custom-format', context).once('date-time').keyup(function () {
       var input = $(this);
       var url = settings.dateTime.lookup + (settings.dateTime.lookup.match(/\?q=/) ? '&format=' : '?format=') + encodeURIComponent(input.val());
       $.getJSON(url, function (data) {
diff --git a/modules/system/system.module b/modules/system/system.module
index b975c859787ab924a773d3f1ebe62a58dc117658..d9a29a0cb5226eaa4688a6a19bc4effcf7eaba67 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -933,6 +933,16 @@ function system_library() {
     ),
   );
 
+  // jQuery Once.
+  $libraries['once'] = array(
+    'title' => 'jQuery Once',
+    'website' => 'http://plugins.jquery.com/project/once',
+    'version' => '1.2',
+    'js' => array(
+      'misc/jquery.once.js' => array('weight' => JS_LIBRARY - 19),
+    ),
+  );
+
   // jQuery Form Plugin.
   $libraries['form'] = array(
     'title' => 'jQuery Form Plugin',
diff --git a/modules/toolbar/toolbar.js b/modules/toolbar/toolbar.js
index 7b804182f9fc74e68ec0632f961ba4cdfe31b464..c1901003939938cb1e812fe87286eb5b6877bd08 100644
--- a/modules/toolbar/toolbar.js
+++ b/modules/toolbar/toolbar.js
@@ -8,18 +8,12 @@ Drupal.behaviors.admin = {
   attach: function() {
 
     // Set the intial state of the toolbar.
-    $('#toolbar:not(.processed)').each(function() {
-      Drupal.admin.toolbar.init();
-      $(this).addClass('processed');
-    });
+    $('#toolbar', context).once('toolbar', Drupal.admin.toolbar.init);
 
     // Toggling of admin shortcuts visibility.
-    $('#toolbar span.toggle:not(.processed)').each(function() {
-      $(this).click(function() {
-        Drupal.admin.toolbar.toggle();
-        return false;
-      });
-      $(this).addClass('processed');
+    $('#toolbar span.toggle', context).once('toolbar-toggle').click(function() {
+      Drupal.admin.toolbar.toggle();
+      return false;
     });
   }
 };
diff --git a/modules/user/user.js b/modules/user/user.js
index 9d75911fae2f16628e4beb5eda690694b8a68214..d382375ab7a80dbe48855afdbbc8dfbd942b3726 100644
--- a/modules/user/user.js
+++ b/modules/user/user.js
@@ -8,8 +8,8 @@
 Drupal.behaviors.password = {
   attach: function (context, settings) {
     var translate = settings.password;
-    $('input.password-field:not(.password-processed)', context).each(function () {
-      var passwordInput = $(this).addClass('password-processed');
+    $('input.password-field', context).once('password', function () {
+      var passwordInput = $(this);
       var innerWrapper = $(this).parent();
       var outerWrapper = $(this).parent().parent();