diff -r 0dfc946e1d3c javafx-ui-common/src/javafx/scene/Node.java
--- a/javafx-ui-common/src/javafx/scene/Node.java	Fri Aug 16 08:40:37 2013 -0700
+++ b/javafx-ui-common/src/javafx/scene/Node.java	Thu Aug 22 12:58:27 2013 +0200
@@ -2932,7 +2932,7 @@
      * really belongs to the Parent of the node and should *only* be modified
      * by the parent.
      */
-    boolean boundsChanged = true;
+    boolean boundsChanged;
 
     /**
      * Returns geometric bounds, but may be over-ridden by a subclass.
diff -r 0dfc946e1d3c javafx-ui-common/src/javafx/scene/Parent.java
--- a/javafx-ui-common/src/javafx/scene/Parent.java	Fri Aug 16 08:40:37 2013 -0700
+++ b/javafx-ui-common/src/javafx/scene/Parent.java	Thu Aug 22 12:58:27 2013 +0200
@@ -27,7 +27,6 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -80,8 +79,8 @@
  *
  */
 public abstract class Parent extends Node {
-
-    private static final int DIRTY_CHILDREN_THRESHOLD = 10;
+    // package private for testing
+    static final int DIRTY_CHILDREN_THRESHOLD = 10;
 
     // If set to true, generate a warning message whenever adding a node to a
     // parent if it is currently a child of another parent.
@@ -122,11 +121,8 @@
             // sides, so we only need to update the remaining portion.
             peer.clearFrom(startIdx);
             for (int idx = startIdx; idx < children.size(); idx++) {
-                    Node n = children.get(idx);
-                    if (n != null) {
-                        peer.add(idx, n.impl_getPGNode());
-                    }
-                }
+                peer.add(idx, children.get(idx).impl_getPGNode());
+            }
             if (removedChildrenExceedsThreshold) {
                 peer.markDirty();
                 removedChildrenExceedsThreshold = false;
@@ -360,12 +356,9 @@
                     if (dirtyChildren != null) {
                         dirtyChildren.remove(old);
                     }
-                    if (old == null) {
-                        continue;
-                    }
-
-                    if (!geomChanged && old.isVisible()) {
-                        geomChanged = childRemoved(old);
+                    if (old.isVisible()) {
+                        geomChanged = true;
+                        childExcluded(old);
                     }
                     if (old.getParent() == Parent.this) {
                         old.setParent(null);
@@ -389,7 +382,7 @@
                     int to = c.getTo();
                     for (int i = from; i < to; ++i) {
                         Node n = children.get(i);
-                        if (n != null && n.getParent() != null && n.getParent() != Parent.this) {
+                        if (n.getParent() != null && n.getParent() != Parent.this) {
                             if (warnOnAutoMove) {
                                 java.lang.System.err.println("WARNING added to a new parent without first removing it from its current");
                                 java.lang.System.err.println("    parent. It will be automatically removed from its current parent.");
@@ -408,8 +401,7 @@
                     List<Node> removed = c.getRemoved();
                     int removedSize = removed.size();
                     for (int i = 0; i < removedSize; ++i) {
-                        Node node = removed.get(i);
-                        if (node != null && node.isManaged()) {
+                        if (removed.get(i).isManaged()) {
                             relayout = true;
                         }
                     }
@@ -417,18 +409,15 @@
                     // update the parent and scene for each new node
                     for (int i = from; i < to; ++i) {
                         Node node = children.get(i);
-                        if (node != null) {
-                            if (node.isManaged()) {
-                                relayout = true;
-                            }
-                            node.setParent(Parent.this);
-                            node.setScene(getScene());
-                            if (dirtyChildren != null) {
-                                dirtyChildren.add(node);
-                            }
-                            if (!geomChanged && node.isVisible()) {
-                                geomChanged = childAdded(node);
-                            }
+                        if (node.isManaged()) {
+                            relayout = true;
+                        }
+                        node.setParent(Parent.this);
+                        node.setScene(getScene());
+                        // assert !node.boundsChanged;
+                        if (node.isVisible()) {
+                            geomChanged = true;
+                            childIncluded(node);
                         }
                     }
                 }
@@ -438,10 +427,11 @@
                 // If so, then we need to create dirtyChildren and
                 // populate it.
                 if (dirtyChildren == null && children.size() > DIRTY_CHILDREN_THRESHOLD) {
-                    dirtyChildren = new LinkedHashSet<Node>(2 * children.size());
+                    dirtyChildren =
+                            new ArrayList<Node>(2 * DIRTY_CHILDREN_THRESHOLD);
                     // only bother populating children if geom has
                     // changed, otherwise there is no need
-                    if (geomChanged) {
+                    if (dirtyChildrenCount > 0) {
                         int size = children.size();
                         for (int i = 0; i < size; ++i) {
                             Node ch = children.get(i);
@@ -546,7 +536,7 @@
     protected <E extends Node> List<E> getManagedChildren() {
         List<E> managed = new ArrayList<E>();
         for (E e : (ObservableList<E>)getChildren()) {
-            if (e != null && e.isManaged()) {
+            if (e.isManaged()) {
                 managed.add(e);
             }
         }
@@ -595,9 +585,7 @@
 
     private void computeDirtyScene(Scene old) {
         for (Node node : getChildren()) {
-            if (node != null) {
-                node.setScene(getScene());
-            }
+            node.setScene(getScene());
         }
 
         if (isNeedsLayout() && old != null) {
@@ -1148,10 +1136,7 @@
         final List kids = this.getChildren();
         final int max = kids.size();
         for (int c=0; c<max; c++) {
-            Node kid = (Node)kids.get(c);
-            if (kid != null) {
-                kid.impl_processCSS(flag);
-            }
+            ((Node)kids.get(c)).impl_processCSS(flag);
         }
     }
     
@@ -1169,10 +1154,7 @@
         final List kids = this.getChildren();
         final int max = kids.size();
         for (int c=0; c<max; c++) {
-            Node kid = (Node)kids.get(c);
-            if (kid != null) {
-                kid.impl_cssResetInitialValues();
-            }
+            ((Node)kids.get(c)).impl_cssResetInitialValues();
         }
     }
     
@@ -1233,12 +1215,18 @@
 
     /**
      * Indicates that the cachedBounds is invalid (or old) and need to be recomputed.
-     * If cachedBoundsInvalid is true, but cachedBounds is not empty,
+     * If cachedBoundsInvalid is true and dirtyChildrenCount is non-zero,
      * then when we recompute the cachedBounds we can consider the
      * values in cachedBounds to represent the last valid bounds for the group.
      * This is useful for several fast paths.
      */
-    private boolean cachedBoundsInvalid = true;
+    private boolean cachedBoundsInvalid;
+
+    /**
+     * The number of dirty children which bounds haven't been incorporated
+     * into the cached bounds yet. Can be used even when dirtyChildren is null.
+     */
+    private int dirtyChildrenCount;
 
     /**
      * This set is used to track all of the children of this group which are
@@ -1248,7 +1236,7 @@
      * null unless the number of children ever crosses the threshold where
      * it will be activated.
      */
-    private LinkedHashSet<Node> dirtyChildren = null;
+    private ArrayList<Node> dirtyChildren;
 
     private Node top;
     private Node left;
@@ -1272,7 +1260,15 @@
             // this is a transform which is only doing translations, or nothing
             // at all (no scales, rotates, or shears)
             // so in this case we can easily use the cached bounds
-            if (cachedBoundsInvalid) recomputeBounds();
+            if (cachedBoundsInvalid) {
+                recomputeBounds();
+
+                if (dirtyChildren != null) {
+                    dirtyChildren.clear();
+                }
+                cachedBoundsInvalid = false;
+                dirtyChildrenCount = 0;
+            }
             if (!tx.isIdentity()) {
                 bounds = bounds.deriveWithNewBounds((float)(cachedBounds.getMinX() + tx.getMxt()),
                                  (float)(cachedBounds.getMinY() + tx.getMyt()),
@@ -1328,401 +1324,286 @@
         }
     }
 
-    // Returns TRUE if the bounds have changed, FALSE otherwise
-    boolean childAdded(Node node) {
-        // If there is no history as to what the bounds were (ie, if
-        // cachedBounds are invalid) then we punt since the next call
-        // to get the bounds will force a full recomputation anyway.
-        
-        // The above comment is not right. With invalid but non-empty bounds 
-        // the recomputation goes through fast-path which doesn't consider
-        // added and removed nodes. I'm turning off this optimizaion.
-
-        //if (cachedBoundsInvalid) return true;
-
-        // If one of the edges has been cleared by the removal of a node,
-        // then there is no point to continuing because we will have to
-        // completely revalidate later.
-        if (top == null || bottom == null || left == null || right == null
-                || near == null || far == null) {
-            cachedBounds.makeEmpty();
-            cachedBoundsInvalid = true;
-            return true;
+    private void setChildDirty(final Node node, final boolean dirty) {
+        if (node.boundsChanged == dirty) {
+            return;
         }
 
-        // If we got here then the top/bottom/left/right/near/far are still set
-        // to a valid node and we know the old size of the Group. So what we are
-        // going to do is execute a fastpath where we figure out if any of the
-        // new Nodes "extend" the bounds, and if so, they will form the new
-        // edges and the cachedBounds will be updated accordingly. We will
-        // also communicate that as a result of adding these Nodes, the
-        // bounds have changed.
+        node.boundsChanged = dirty;
+        if (dirty) {
+            if (dirtyChildren != null) {
+                dirtyChildren.add(node);
+            }
+            ++dirtyChildrenCount;
+        } else {
+            if (dirtyChildren != null) {
+                dirtyChildren.remove(node);
+            }
+            --dirtyChildrenCount;
+        }
+    }
 
-        boolean extended = false;
-
-        // set to indicate which edges need to be cleared so we can recompute
-        if (node.isVisible()) {
-            tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
-            if (!tmp.isEmpty()) {
-                node.boundsChanged = false;
-                double tmpx = tmp.getMinX();
-                double tmpy = tmp.getMinY();
-                double tmpz = tmp.getMinZ();
-                double tmpx2 = tmp.getMaxX();
-                double tmpy2 = tmp.getMaxY();
-                double tmpz2 = tmp.getMaxZ();
-                double cx = cachedBounds.getMinX();
-                double cy = cachedBounds.getMinY();
-                double cz = cachedBounds.getMinZ();
-                double cx2 = cachedBounds.getMaxX();
-                double cy2 = cachedBounds.getMaxY();
-                double cz2 = cachedBounds.getMaxZ();
-
-                double minX = Math.min(tmpx, cx);
-                double minY = Math.min(tmpy, cy);
-                double minZ = Math.min(tmpz, cz);
-                double maxX = Math.max(tmpx2, cx2);
-                double maxY = Math.max(tmpy2, cy2);
-                double maxZ = Math.max(tmpz2, cz2);
-
-                if (tmpy < cy) { extended = true; top = node; }
-                if (tmpx < cx) { extended = true; left = node; }
-                if (tmpz < cz) { extended = true; near = node; }
-                if (tmpy2 > cy2) { extended = true; bottom = node; }
-                if (tmpx2 > cx2) { extended = true; right = node; }
-                if (tmpz2 > cz2) { extended = true; far = node; }
-
-                // update the cached bounds
-                cachedBounds = cachedBounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
-                        (float)maxX, (float)maxY, (float)maxZ);
-            }
-        }
-        // return whether or not the bounds have been extended (and thus, are
-        // dirty)
-        return extended;
+    private void childIncluded(final Node node) {
+        // assert node.isVisible();
+        cachedBoundsInvalid = true;
+        setChildDirty(node, true);
     }
 
     // This is called when either the child is actually removed, OR IF IT IS
     // TOGGLED TO BE INVISIBLE. This is because in both cases it needs to be
     // cleared from the state which manages bounds.
-    boolean childRemoved(Node node) {
-        // clear this flag because Group no longer knows whether the bounds
-        // have changed or not relative to itself
-        node.boundsChanged = false;
-        // If there is no history as to what the bounds were (ie, if
-        // cachedBounds are invalid) then we punt since the next call
-        // to get the bounds will force a full recomputation anyway.
-
-        // The above comment is not right. With invalid but non-empty bounds 
-        // the recomputation goes through fast-path which doesn't consider
-        // added and removed nodes. I'm turning off this optimizaion.
-
-        // if (cachedBoundsInvalid) return true;
-
-        // Since there was some prior size to the Group, all the nodes that
-        // were removed are checked to see if they formed one of the edges. If
-        // so, the edge variable is cleared
-        if (node == top) top = null;
-        if (node == left) left = null;
-        if (node == bottom) bottom = null;
-        if (node == right) right = null;
-        if (node == near) near = null;
-        if (node == far) far = null;
-
-        // If one of the edges has been cleared by the removal of a node,
-        // then there is no point to continuing because we will have to
-        // completely revalidate later.
-        if (top == null || bottom == null || left == null || right == null
-                || near == null || far == null) {
-            cachedBounds.makeEmpty();
+    private void childExcluded(final Node node) {
+        if (node == left) {
+            left = null;
             cachedBoundsInvalid = true;
-            top = left = bottom = right = near = far = null;
-            return true;
+        }
+        if (node == top) {
+            top = null;
+            cachedBoundsInvalid = true;
+        }
+        if (node == near) {
+            near = null;
+            cachedBoundsInvalid = true;
+        }
+        if (node == right) {
+            right = null;
+            cachedBoundsInvalid = true;
+        }
+        if (node == bottom) {
+            bottom = null;
+            cachedBoundsInvalid = true;
+        }
+        if (node == far) {
+            far = null;
+            cachedBoundsInvalid = true;
         }
 
-        // Return false since nothing has changed that will affect the bounds.
-        return false;
+        setChildDirty(node, false);
     }
 
     /**
      * Recomputes the bounds from scratch and saves the cached bounds.
      */
-    void recomputeBounds() {
+    private void recomputeBounds() {
         // fast path for case of no children
         if (children.isEmpty()) {
-            if (dirtyChildren != null) dirtyChildren.clear();
-            cachedBoundsInvalid = false;
             cachedBounds.makeEmpty();
             return;
         }
 
         // fast path for case of 1 child
         if (children.size() == 1) {
-            if (dirtyChildren != null) dirtyChildren.clear();
-            cachedBoundsInvalid = false;
             Node node = children.get(0);
-            if (node == null) return;
-
+            node.boundsChanged = false;
             if (node.isVisible()) {
-                cachedBounds = node.getTransformedBounds(cachedBounds, BaseTransform.IDENTITY_TRANSFORM);
+                cachedBounds = node.getTransformedBounds(
+                                        cachedBounds,
+                                        BaseTransform.IDENTITY_TRANSFORM);
+                top = left = bottom = right = near = far = node;
             } else {
                 cachedBounds.makeEmpty();
+                // no need to null edge nodes here, it was done in childExcluded
+                // top = left = bottom = right = near = far = null;
             }
-            node.boundsChanged = false;
             return;
         }
 
-        // We will attempt to use a fastpath through this function. We can use
-        // the fastpath as long as we have an old size of the Group recorded
-        // (cachedBounds is invalid but not empty), and each of the edge
-        // variables have a value.
-        if (cachedBounds != null
-            && !cachedBounds.isEmpty()
-            && top != null
-            && left != null
-            && bottom != null
-            && right != null
-            && near != null
-            && far != null)
-        {
-            // The fastpath will simply iterate over each of the Nodes in the
-            // group. If any node has "dirty" bounds, then I know that I must
-            // recompute its bounds. When I do so, I will take special care
-            // if it was once one of my edges, and also special care if it has
-            // moved to become a new edge.
+        if ((dirtyChildrenCount == 0) ||
+                !updateCachedBounds(dirtyChildren != null
+                                        ? dirtyChildren : children,
+                                    dirtyChildrenCount)) {
+            // failed to update cached bounds, recreate them
+            createCachedBounds(children);
+        }
+    }
 
-            // First, look at the nodes which form the top, left, bottom, right,
-            // near and far edges. If any of them are now invisible (visible = false)
-            // then they no longer contribute to the bounds of the group and
-            // therefore, this fast path cannot be used
-            if (top.isVisible() && left.isVisible() && bottom.isVisible() && right.isVisible()
-                    && near.isVisible() && far.isVisible()) {
-                // This fastpath becomes degenerate if I get to the end only to
-                // discover that the very last node has moved inward, in which
-                // case I have to throw away my work and simply sum up all the
-                // nodes in the group.
-                boolean quit = false;
+    private final int LEFT_INVALID = 1;
+    private final int TOP_INVALID = 1 << 1;
+    private final int NEAR_INVALID = 1 << 2;
+    private final int RIGHT_INVALID = 1 << 3;
+    private final int BOTTOM_INVALID = 1 << 4;
+    private final int FAR_INVALID = 1 << 5;
 
-                // These are local references to the cached bounds vars
-                double cx = cachedBounds.getMinX();
-                double cy = cachedBounds.getMinY();
-                double cz = cachedBounds.getMinZ();
-                double cx2 = cachedBounds.getMaxX();
-                double cy2 = cachedBounds.getMaxY();
-                double cz2 = cachedBounds.getMaxZ();
-
-                // These indicate the bounds of the Group as computed by this
-                // function (had to add the 'b' suffix to suppress a spurious
-                // compiler error message)
-                double minXb=cx;
-                double minYb=cy;
-                double minZb=cz;
-                double maxXb=cx2;
-                double maxYb=cy2;
-                double maxZb=cz2;
-
-                // iterate over all the dirty nodes
-                if (dirtyChildren != null) {
-                    for (Node node : dirtyChildren) {
-                        if (node.isVisible() && node.boundsChanged) {
-                            tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
-                            if (tmp.isEmpty()) {
-                                // If this node formed one of the edges, and if it has
-                                // moved off that edge (inward), then we break out of this
-                                // loop and quit the fast path
-                                if (node == top) { quit = true; break; }
-                                if (node == left) { quit = true; break; }
-                                if (node == bottom) { quit = true; break; }
-                                if (node == right) { quit = true; break; }
-                                if (node == near) { quit = true; break; }
-                                if (node == far) { quit = true; break; }
-                            } else {
-                                double tmpx = tmp.getMinX();
-                                double tmpy = tmp.getMinY();
-                                double tmpz = tmp.getMinZ();
-                                double tmpx2 = tmp.getMaxX();
-                                double tmpy2 = tmp.getMaxY();
-                                double tmpz2 = tmp.getMaxZ();
-
-                                // If this node formed one of the edges, and if it has
-                                // moved off that edge (inward), then we break out of this
-                                // loop and quit the fast path
-                                if (node == top && tmpy > cy) { quit = true; break; }
-                                if (node == left && tmpx > cx) { quit = true; break; }
-                                if (node == near && tmpz > cz) { quit = true; break; }
-                                if (node == bottom && tmpy2 < cy2) { quit = true; break; }
-                                if (node == right && tmpx2 < cx2) { quit = true; break; }
-                                if (node == far && tmpz2 < cz2) { quit = true; break; }
-
-                                // If this node forms an edge, then we will set it to be the
-                                // node for this edge and update the min/max values
-                                if (tmpy < minYb) { minYb = tmpy; top = node; }
-                                if (tmpx < minXb) { minXb = tmpx; left = node; }
-                                if (tmpz < minZb) { minZb = tmpz; near = node; }
-                                if (tmpy2 > maxYb) { maxYb = tmpy2; bottom = node; }
-                                if (tmpx2 > maxXb) { maxXb = tmpx2; right = node; }
-                                if (tmpz2 > maxZb) { maxZb = tmpz2; far = node; }
-                            }
-                        }
-                        node.boundsChanged = false;
-                    }
-                } else {
-                    final ObservableList<Node> _children_ = getChildren();
-                    final int count = _children_.size();
-                    for (int i=0; i<count; i++) {
-                        final Node node = _children_.get(i);
-                        if (node == null)
-                            continue;
-                        if (node.isVisible() && node.boundsChanged) {
-                            tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
-                            if (tmp.isEmpty()) {
-                                // If this node formed one of the edges, and if it has
-                                // moved off that edge (inward), then we break out of this
-                                // loop and quit the fast path
-                                if (node == top) { quit = true; break; }
-                                if (node == left) { quit = true; break; }
-                                if (node == bottom) { quit = true; break; }
-                                if (node == right) { quit = true; break; }
-                                if (node == near) { quit = true; break; }
-                                if (node == far) { quit = true; break; }
-                            } else {
-                                double tmpx = tmp.getMinX();
-                                double tmpy = tmp.getMinY();
-                                double tmpz = tmp.getMinZ();
-                                double tmpx2 = tmp.getMaxX();
-                                double tmpy2 = tmp.getMaxY();
-                                double tmpz2 = tmp.getMaxZ();
-
-                                // If this node formed one of the edges, and if it has
-                                // moved off that edge (inward), then we break out of this
-                                // loop and quit the fast path
-                                if (node == top && tmpy > cy) { quit = true; break; }
-                                if (node == left && tmpx > cx) { quit = true; break; }
-                                if (node == near && tmpz > cz) { quit = true; break; }
-                                if (node == bottom && tmpy2 < cy2) { quit = true; break; }
-                                if (node == right && tmpx2 < cx2) { quit = true; break; }
-                                if (node == far && tmpz2 < cz2) { quit = true; break; }
-
-                                // If this node forms an edge, then we will set it to be the
-                                // node for this edge and update the min/max values
-                                if (tmpy < minYb) { minYb = tmpy; top = node; }
-                                if (tmpx < minXb) { minXb = tmpx; left = node; }
-                                if (tmpz < minZb) { minZb = tmpz; near = node; }
-                                if (tmpy2 > maxYb) { maxYb = tmpy2; bottom = node; }
-                                if (tmpx2 > maxXb) { maxXb = tmpx2; right = node; }
-                                if (tmpz2 > maxZb) { maxZb = tmpz2; far = node; }
-                            }
-                        }
-                        node.boundsChanged = false;
-                    }
-                }
-
-                if (dirtyChildren != null) dirtyChildren.clear();
-
-                if (!quit) {
-                    // The fastpath succeeded, so we can simply update the
-                    // cachedBounds, clear the cachedBoundsInvalid flag and return
-                    cachedBoundsInvalid = false;
-                    cachedBounds = cachedBounds.deriveWithNewBounds((float)minXb, (float)minYb, (float)minZb,
-                            (float)maxXb, (float)maxYb, (float)maxZb);
-                    return;
-                }
-            }
-            // fall through to the long case
+    private boolean updateCachedBounds(final List<Node> dirtyNodes,
+                                       int remainingDirtyNodes) {
+        // fast path for untransformed bounds calculation
+        if (cachedBounds.isEmpty()) {
+            createCachedBounds(dirtyNodes);
+            return true;
         }
 
-        // These indicate the bounds of the Group as computed by this function
-        double minX = 0;
-        double minY = 0;
-        double minZ = 0;
-        double maxX = 0;
-        double maxY = 0;
-        double maxZ = 0;
+        int invalidEdges = 0;
 
-        // The long case is pretty simple -- visit each and every node, get
-        // the bounds of each node, figure out the edge nodes, get the min/max
-        // for the x/y/x2/y2 and then update cachedBounds and cachedBoundsInvalid
-        boolean first = true;
-        final ObservableList<Node> _children_ = getChildren();
-        final int count = _children_.size();
-        for (int i=0; i<count; i++) {
-            final Node node = _children_.get(i);
-            if (node == null)
-                continue;
+        if ((left == null) || left.boundsChanged) {
+            invalidEdges |= LEFT_INVALID;
+        }
+        if ((top == null) || top.boundsChanged) {
+            invalidEdges |= TOP_INVALID;
+        }
+        if ((near == null) || near.boundsChanged) {
+            invalidEdges |= NEAR_INVALID;
+        }
+        if ((right == null) || right.boundsChanged) {
+            invalidEdges |= RIGHT_INVALID;
+        }
+        if ((bottom == null) || bottom.boundsChanged) {
+            invalidEdges |= BOTTOM_INVALID;
+        }
+        if ((far == null) || far.boundsChanged) {
+            invalidEdges |= FAR_INVALID;
+        }
 
-            node.boundsChanged = false;
-            if (node.isVisible()) {
-                tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
+        // These indicate the bounds of the Group as computed by this
+        // function
+        float minX = cachedBounds.getMinX();
+        float minY = cachedBounds.getMinY();
+        float minZ = cachedBounds.getMinZ();
+        float maxX = cachedBounds.getMaxX();
+        float maxY = cachedBounds.getMaxY();
+        float maxZ = cachedBounds.getMaxZ();
+
+        // this checks the newly added nodes first, so if dirtyNodes is the
+        // whole children list, we can end early
+        for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) {
+            final Node node = dirtyNodes.get(i);
+            if (node.boundsChanged) {
+                // assert node.isVisible();
+                node.boundsChanged = false;
+                --remainingDirtyNodes;
+                tmp = node.getTransformedBounds(
+                               tmp, BaseTransform.IDENTITY_TRANSFORM);
                 if (!tmp.isEmpty()) {
-                    double tmpx = tmp.getMinX();
-                    double tmpy = tmp.getMinY();
-                    double tmpz = tmp.getMinZ();
-                    double tmpx2 = tmp.getMaxX();
-                    double tmpy2 = tmp.getMaxY();
-                    double tmpz2 = tmp.getMaxZ();
+                    float tmpx = tmp.getMinX();
+                    float tmpy = tmp.getMinY();
+                    float tmpz = tmp.getMinZ();
+                    float tmpx2 = tmp.getMaxX();
+                    float tmpy2 = tmp.getMaxY();
+                    float tmpz2 = tmp.getMaxZ();
 
-                    if (first) {
-                        top = left = bottom = right = near = far = node;
+                    // If this node forms an edge, then we will set it to be the
+                    // node for this edge and update the min/max values
+                    if (tmpx <= minX) {
                         minX = tmpx;
+                        left = node;
+                        invalidEdges &= ~LEFT_INVALID;
+                    }
+                    if (tmpy <= minY) {
                         minY = tmpy;
+                        top = node;
+                        invalidEdges &= ~TOP_INVALID;
+                    }
+                    if (tmpz <= minZ) {
                         minZ = tmpz;
+                        near = node;
+                        invalidEdges &= ~NEAR_INVALID;
+                    }
+                    if (tmpx2 >= maxX) {
                         maxX = tmpx2;
+                        right = node;
+                        invalidEdges &= ~RIGHT_INVALID;
+                    }
+                    if (tmpy2 >= maxY) {
                         maxY = tmpy2;
+                        bottom = node;
+                        invalidEdges &= ~BOTTOM_INVALID;
+                    }
+                    if (tmpz2 >= maxZ) {
                         maxZ = tmpz2;
-                        first = false;
-                    } else {
-                        if (tmpy < minY) { minY = tmpy; top = node; }
-                        if (tmpx < minX) { minX = tmpx; left = node; }
-                        if (tmpz < minZ) { minZ = tmpz; near = node; }
-                        if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; }
-                        if (tmpx2 > maxX) { maxX = tmpx2; right = node; }
-                        if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; }
+                        far = node;
+                        invalidEdges &= ~FAR_INVALID;
                     }
                 }
             }
         }
 
-        if (dirtyChildren != null) dirtyChildren.clear();
-        cachedBoundsInvalid = false;
-        if (first) {
-            top = left = bottom = right = near = far = null;
-            cachedBounds.makeEmpty();
-        } else {
-            cachedBounds = cachedBounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
-                    (float)maxX, (float)maxY, (float)maxZ);
+        if (invalidEdges != 0) {
+            // failed to validate some edges
+            return false;
         }
+
+        cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
+                                                        maxX, maxY, maxZ);
+        return true;
     }
 
-    /**
-     * Overridden to make sure boundsInvalid gets set to true
-     *
-     * @treatAsPrivate implementation detail
-     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
-     */
-    @Deprecated
-    @Override protected void impl_geomChanged() {
-        cachedBoundsInvalid = true;
-        super.impl_geomChanged();
+    private void createCachedBounds(final List<Node> fromNodes) {
+        // These indicate the bounds of the Group as computed by this function
+        float minX, minY, minZ;
+        float maxX, maxY, maxZ;
+
+        final int nodeCount = fromNodes.size();
+        int i;
+
+        // handle first visible non-empty node
+        for (i = 0; i < nodeCount; ++i) {
+            final Node node = fromNodes.get(i);
+            node.boundsChanged = false;
+            if (node.isVisible()) {
+                tmp = node.getTransformedBounds(
+                               tmp, BaseTransform.IDENTITY_TRANSFORM);
+                if (!tmp.isEmpty()) {
+                    left = top = near = right = bottom = far = node;
+                    break;
+                }
+            }
+        }
+
+        if (i == nodeCount) {
+            left = top = near = right = bottom = far = null;
+            cachedBounds.makeEmpty();
+            return;
+        }
+
+        minX = tmp.getMinX();
+        minY = tmp.getMinY();
+        minZ = tmp.getMinZ();
+        maxX = tmp.getMaxX();
+        maxY = tmp.getMaxY();
+        maxZ = tmp.getMaxZ();
+
+        // handle remaining visible non-empty nodes
+        for (++i; i < nodeCount; ++i) {
+            final Node node = fromNodes.get(i);
+            node.boundsChanged = false;
+            if (node.isVisible()) {
+                tmp = node.getTransformedBounds(
+                               tmp, BaseTransform.IDENTITY_TRANSFORM);
+                if (!tmp.isEmpty()) {
+                    final float tmpx = tmp.getMinX();
+                    final float tmpy = tmp.getMinY();
+                    final float tmpz = tmp.getMinZ();
+                    final float tmpx2 = tmp.getMaxX();
+                    final float tmpy2 = tmp.getMaxY();
+                    final float tmpz2 = tmp.getMaxZ();
+
+                    if (tmpx < minX) { minX = tmpx; left = node; }
+                    if (tmpy < minY) { minY = tmpy; top = node; }
+                    if (tmpz < minZ) { minZ = tmpz; near = node; }
+                    if (tmpx2 > maxX) { maxX = tmpx2; right = node; }
+                    if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; }
+                    if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; }
+                }
+            }
+        }
+
+        cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
+                                                        maxX, maxY, maxZ);
     }
 
     /**
      * Called by Node whenever its bounds have changed.
      */
     void childBoundsChanged(Node node) {
-        if (!node.isVisible()) {
-            // the change in bounds of this child node has no bearing on
-            // the bounds of the group, so forget it
-            return;
-        }
+        // assert node.isVisible();
+
+        cachedBoundsInvalid = true;
 
         // mark the node such that the parent knows that the child's bounds
         // are not in sync with this parent. In this way, when the bounds
         // need to be computed, we'll come back and figure out the new bounds
         // for all the children which have boundsChanged set to true
-        if (!node.boundsChanged) {
-            node.boundsChanged = true;
-            if (dirtyChildren != null) dirtyChildren.add(node);
-        }
+        setChildDirty(node, true);
+
         // go ahead and indicate that the geom has changed for this parent,
         // even though once we figure it all out it may be that the bounds
         // have not changed
@@ -1733,8 +1614,12 @@
      * Called by node whenever the visibility of the node changes.
      */
     void childVisibilityChanged(Node node) {
-        // cachedBoundsInvalid will be set to true in impl_geomChanged();
-        cachedBounds.makeEmpty();
+        if (node.isVisible()) {
+            childIncluded(node);
+        } else {
+            childExcluded(node);
+        }
+
         impl_geomChanged();
     }
 
diff -r 0dfc946e1d3c javafx-ui-common/test/unit/javafx/scene/Parent_recomputeBounds_Test.java
--- a/javafx-ui-common/test/unit/javafx/scene/Parent_recomputeBounds_Test.java	Fri Aug 16 08:40:37 2013 -0700
+++ b/javafx-ui-common/test/unit/javafx/scene/Parent_recomputeBounds_Test.java	Thu Aug 22 12:58:27 2013 +0200
@@ -30,6 +30,7 @@
 import static org.junit.Assert.assertTrue;
 import javafx.geometry.Bounds;
 import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Circle;
 
 import org.junit.Test;
 
@@ -375,4 +376,154 @@
         assertEquals(300, b.getWidth(), 0.0001);
         assertEquals(300, b.getHeight(), 0.0001);
     }
+
+    @Test
+    public void shouldNotStartBoundsCalculationFromEmptyBounds() {
+        final Group g = new Group();
+        final Rectangle lt = new Rectangle(50, 50, 50, 50);
+        final Rectangle rb = new Rectangle(100, 100, 50, 50);
+        Bounds b;
+
+        g.getChildren().addAll(lt, rb);
+
+        b = g.getBoundsInLocal();
+        assertEquals(50, b.getMinX(), 0.0001);
+        assertEquals(50, b.getMinY(), 0.0001);
+        assertEquals(100, b.getWidth(), 0.0001);
+        assertEquals(100, b.getHeight(), 0.0001);
+
+        // invalidate (empty) bounds
+        rb.setVisible(false);
+
+        // add new rectangle, bounds should not be recalculated by using
+        // empty bounds
+        g.getChildren().add(new Rectangle(150, 150, 50, 50));
+
+        b = g.getBoundsInLocal();
+        assertEquals(50, b.getMinX(), 0.0001);
+        assertEquals(50, b.getMinY(), 0.0001);
+        assertEquals(150, b.getWidth(), 0.0001);
+        assertEquals(150, b.getHeight(), 0.0001);
+    }
+
+    @Test
+    public void shouldNotIgnoreMultipleAddedNodes() {
+        final Group g = new Group();
+        final Rectangle lt = new Rectangle(100, 100, 100, 100);
+        final Rectangle mt = new Rectangle(200, 100, 100, 100);
+        final Rectangle rt = new Rectangle(300, 100, 100, 100);
+        final Rectangle rb = new Rectangle(300, 200, 100, 100);
+        Bounds b;
+
+        g.getChildren().addAll(lt, mt);
+
+        b = g.getBoundsInLocal();
+        assertEquals(100, b.getMinX(), 0.0001);
+        assertEquals(100, b.getMinY(), 0.0001);
+        assertEquals(200, b.getWidth(), 0.0001);
+        assertEquals(100, b.getHeight(), 0.0001);
+
+        ((Node) rb).boundsChanged = false;
+        // rt, rb should be either incorporated into parent bounds directly
+        // or marked as dirty for parent bounds calculation
+        g.getChildren().addAll(rt, rb);
+
+        b = g.getBoundsInLocal();
+        assertEquals(100, b.getMinX(), 0.0001);
+        assertEquals(100, b.getMinY(), 0.0001);
+        assertEquals(300, b.getWidth(), 0.0001);
+        assertEquals(200, b.getHeight(), 0.0001);
+    }
+
+    @Test
+    public void shouldNotCreateEmptyDirtyChildrenList() {
+        final Group g = new Group();
+        final Rectangle lt = new Rectangle(100, 100, 100, 100);
+        final Rectangle rt = new Rectangle(200, 100, 100, 100);
+        final Rectangle lb = new Rectangle(100, 200, 100, 100);
+        final Rectangle rb = new Rectangle(200, 200, 100, 100);
+        Bounds b;
+
+        g.getChildren().addAll(lt, rt, lb, rb);
+        final int toAdd = Parent.DIRTY_CHILDREN_THRESHOLD - 4;
+        for (int i = 0; i < toAdd; ++i) {
+            g.getChildren().add(
+                    new Rectangle(150 + i * 80 / (toAdd - 1), 190, 20, 20));
+        }
+
+        b = g.getBoundsInLocal();
+        assertEquals(100, b.getMinX(), 0.0001);
+        assertEquals(100, b.getMinY(), 0.0001);
+        assertEquals(200, b.getWidth(), 0.0001);
+        assertEquals(200, b.getHeight(), 0.0001);
+
+        lt.setX(50);
+        // this should create a dirty children list on Parent, even though
+        // the added node doesn't change the group gemetry, the created
+        // dirty children list should still contain the previously modified
+        // corner node (lt)
+        g.getChildren().add(new Rectangle(150, 150, 100, 100));
+
+        b = g.getBoundsInLocal();
+        assertEquals(50, b.getMinX(), 0.0001);
+        assertEquals(100, b.getMinY(), 0.0001);
+        assertEquals(200, b.getWidth(), 0.0001);
+        assertEquals(200, b.getHeight(), 0.0001);
+    }
+
+    @Test
+    public void
+            shouldNotSkipGeomChangedForChildAdditionInsideUntransformedBounds()
+    {
+        final Group g = new Group(new Circle(0, -100, 0.001),
+                                  new Circle(0, 100, 0.001),
+                                  new Circle(-100, 0, 0.001),
+                                  new Circle(100, 0, 0.001));
+        g.getTransforms().add(new Rotate(-45));
+
+        Bounds b;
+
+        b = g.getBoundsInParent();
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinY(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getHeight(), 0.1);
+
+        g.getChildren().add(new Circle(95, -95, 0.001));
+
+        b = g.getBoundsInParent();
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
+        assertEquals(-95 * Math.sqrt(2), b.getMinY(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
+        assertEquals((50 + 95) * Math.sqrt(2), b.getHeight(), 0.1);
+    }
+
+    @Test
+    public void
+            shouldNotSkipGeomChangedForChildRemovalInsideUntransformedBounds()
+    {
+        final Circle toRemove = new Circle(95, -95, 0.001);
+        final Group g = new Group(toRemove,
+                                  new Circle(0, -100, 0.001),
+                                  new Circle(0, 100, 0.001),
+                                  new Circle(-100, 0, 0.001),
+                                  new Circle(100, 0, 0.001));
+        g.getTransforms().add(new Rotate(-45));
+
+        Bounds b;
+
+        b = g.getBoundsInParent();
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
+        assertEquals(-95 * Math.sqrt(2), b.getMinY(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
+        assertEquals((50 + 95) * Math.sqrt(2), b.getHeight(), 0.1);
+
+        g.getChildren().remove(toRemove);
+
+        b = g.getBoundsInParent();
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinX(), 0.1);
+        assertEquals(-100 * Math.sqrt(2) / 2, b.getMinY(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getWidth(), 0.1);
+        assertEquals(100 * Math.sqrt(2), b.getHeight(), 0.1);
+    }
 }
