diff --git a/package-lock.json b/package-lock.json
index ffc4b18b..307e4b5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1439,9 +1439,9 @@
       }
     },
     "lib0": {
-      "version": "0.2.26",
-      "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.26.tgz",
-      "integrity": "sha512-DTf0VmFNi/eT+3Q+6rNHYdIAx69ROpvQkpnplpDoErW8NeRwjPwoIKjCF3rKebsMrQoxH4tFD1bvMQb4CUzcFg==",
+      "version": "0.2.27",
+      "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.27.tgz",
+      "integrity": "sha512-fjvdUOqdpm5DixZVWppYysbaXb97yHDSqYnNHFVVwPPA4qeQsGZQgWSitG+XhPRsltSPOQHILLWiD43NRKqsMw==",
       "requires": {
         "isomorphic.js": "^0.1.3"
       }
diff --git a/package.json b/package.json
index 72f12099..c3de97cb 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
   },
   "homepage": "https://yjs.dev",
   "dependencies": {
-    "lib0": "^0.2.26"
+    "lib0": "^0.2.27"
   },
   "devDependencies": {
     "@rollup/plugin-commonjs": "^11.0.1",
diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js
index ef4acee1..21995ad1 100644
--- a/src/structs/AbstractStruct.js
+++ b/src/structs/AbstractStruct.js
@@ -12,11 +12,6 @@ export class AbstractStruct {
    * @param {number} length
    */
   constructor (id, length) {
-    /**
-     * The uniqe identifier of this struct.
-     * @type {ID}
-     * @readonly
-     */
     this.id = id
     this.length = length
     this.deleted = false
@@ -55,15 +50,11 @@ export class AbstractStructRef {
    * @param {ID} id
    */
   constructor (id) {
+    this.id = id
     /**
      * @type {Array<ID>}
      */
     this._missing = []
-    /**
-     * The uniqe identifier of this type.
-     * @type {ID}
-     */
-    this.id = id
   }
 
   /**
diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js
index 46944681..e1dd7483 100644
--- a/src/structs/ContentType.js
+++ b/src/structs/ContentType.js
@@ -7,7 +7,7 @@ import {
   readYXmlFragment,
   readYXmlHook,
   readYXmlText,
-  StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
+  ID, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
 } from '../internals.js'
 
 import * as encoding from 'lib0/encoding.js' // eslint-disable-line
@@ -115,7 +115,7 @@ export class ContentType {
         // We try to merge all deleted items after each transaction,
         // but we have no knowledge about that this needs to be merged
         // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
-        transaction._mergeStructs.add(item.id)
+        transaction._mergeStructs.push(item)
       }
       item = item.right
     }
@@ -124,7 +124,7 @@ export class ContentType {
         item.delete(transaction)
       } else {
         // same as above
-        transaction._mergeStructs.add(item.id)
+        transaction._mergeStructs.push(item)
       }
     })
     transaction.changed.delete(this.type)
diff --git a/src/structs/GC.js b/src/structs/GC.js
index 0105f613..b70b27a3 100644
--- a/src/structs/GC.js
+++ b/src/structs/GC.js
@@ -2,7 +2,6 @@
 import {
   AbstractStructRef,
   AbstractStruct,
-  createID,
   addStruct,
   StructStore, Transaction, ID // eslint-disable-line
 } from '../internals.js'
@@ -78,8 +77,7 @@ export class GCRef extends AbstractStructRef {
    */
   toStruct (transaction, store, offset) {
     if (offset > 0) {
-      // @ts-ignore
-      this.id = createID(this.id.client, this.id.clock + offset)
+      this.id.clock += offset
       this.length -= offset
     }
     return new GC(
diff --git a/src/structs/Item.js b/src/structs/Item.js
index 15a81daa..702c814e 100644
--- a/src/structs/Item.js
+++ b/src/structs/Item.js
@@ -1,10 +1,9 @@
 
 import {
   readID,
-  createID,
   writeID,
   GC,
-  nextID,
+  getState,
   AbstractStructRef,
   AbstractStruct,
   replaceStruct,
@@ -21,6 +20,7 @@ import {
   readContentAny,
   readContentString,
   readContentEmbed,
+  createID,
   readContentFormat,
   readContentType,
   addChangedTypeToTransaction,
@@ -88,12 +88,12 @@ export const keepItem = (item, keep) => {
  * @private
  */
 export const splitItem = (transaction, leftItem, diff) => {
-  const id = leftItem.id
   // create rightItem
+  const { client, clock } = leftItem.id
   const rightItem = new Item(
-    createID(id.client, id.clock + diff),
+    createID(client, clock + diff),
     leftItem,
-    createID(id.client, id.clock + diff - 1),
+    createID(client, clock + diff - 1),
     leftItem.right,
     leftItem.rightOrigin,
     leftItem.parent,
@@ -116,7 +116,7 @@ export const splitItem = (transaction, leftItem, diff) => {
     rightItem.right.left = rightItem
   }
   // right is more specific.
-  transaction._mergeStructs.add(rightItem.id)
+  transaction._mergeStructs.push(rightItem)
   // update parent._map
   if (rightItem.parentSub !== null && rightItem.right === null) {
     rightItem.parent._map.set(rightItem.parentSub, rightItem)
@@ -137,8 +137,12 @@ export const splitItem = (transaction, leftItem, diff) => {
  * @private
  */
 export const redoItem = (transaction, item, redoitems) => {
-  if (item.redone !== null) {
-    return getItemCleanStart(transaction, item.redone)
+  const doc = transaction.doc
+  const store = doc.store
+  const ownClientID = doc.clientID
+  const redone = item.redone
+  if (redone !== null) {
+    return getItemCleanStart(transaction, redone)
   }
   let parentItem = item.parent._item
   /**
@@ -158,7 +162,7 @@ export const redoItem = (transaction, item, redoitems) => {
     left = item
     while (left.right !== null) {
       left = left.right
-      if (left.id.client !== transaction.doc.clientID) {
+      if (left.id.client !== ownClientID) {
         // It is not possible to redo this item because it conflicts with a
         // change from another client
         return null
@@ -212,15 +216,17 @@ export const redoItem = (transaction, item, redoitems) => {
       right = right.right
     }
   }
+  const nextClock = getState(store, ownClientID)
+  const nextId = createID(ownClientID, nextClock)
   const redoneItem = new Item(
-    nextID(transaction),
-    left, left === null ? null : left.lastId,
-    right, right === null ? null : right.id,
+    nextId,
+    left, left && left.lastId,
+    right, right && right.id,
     parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
     item.parentSub,
     item.content.copy()
   )
-  item.redone = redoneItem.id
+  item.redone = nextId
   keepItem(redoneItem, true)
   redoneItem.integrate(transaction)
   return redoneItem
@@ -294,30 +300,35 @@ export class Item extends AbstractStruct {
      * @type {AbstractContent}
      */
     this.content = content
-    this.length = content.getLength()
-    this.countable = content.isCountable()
     /**
      * If true, do not garbage collect this Item.
      */
     this.keep = false
   }
 
+  get countable () {
+    return this.content.isCountable()
+  }
+
   /**
    * @param {Transaction} transaction
    */
   integrate (transaction) {
     const store = transaction.doc.store
-    const id = this.id
     const parent = this.parent
     const parentSub = this.parentSub
     const length = this.length
+    /**
+     * @type {Item|null}
+     */
+    let left = this.left
     /**
      * @type {Item|null}
      */
     let o
     // set o to the first conflicting item
-    if (this.left !== null) {
-      o = this.left.right
+    if (left !== null) {
+      o = left.right
     } else if (parentSub !== null) {
       o = parent._map.get(parentSub) || null
       while (o !== null && o.left !== null) {
@@ -343,14 +354,14 @@ export class Item extends AbstractStruct {
       conflictingItems.add(o)
       if (compareIDs(this.origin, o.origin)) {
         // case 1
-        if (o.id.client < id.client) {
-          this.left = o
+        if (o.id.client < this.id.client) {
+          left = o
           conflictingItems.clear()
         }
       } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
         // case 2
         if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
-          this.left = o
+          left = o
           conflictingItems.clear()
         }
       } else {
@@ -358,11 +369,12 @@ export class Item extends AbstractStruct {
       }
       o = o.right
     }
+    this.left = left
     // reconnect left/right + update parent map/start if necessary
-    if (this.left !== null) {
-      const right = this.left.right
+    if (left !== null) {
+      const right = left.right
       this.right = right
-      this.left.right = this
+      left.right = this
     } else {
       let r
       if (parentSub !== null) {
@@ -381,9 +393,9 @@ export class Item extends AbstractStruct {
     } else if (parentSub !== null) {
       // set as current parent value if right === null and this is parentSub
       parent._map.set(parentSub, this)
-      if (this.left !== null) {
+      if (left !== null) {
         // this is the current attribute value of parent. delete right
-        this.left.delete(transaction)
+        left.delete(transaction)
       }
     }
     // adjust length of parent
@@ -522,7 +534,8 @@ export class Item extends AbstractStruct {
     }
     if (origin === null && rightOrigin === null) {
       const parent = this.parent
-      if (parent._item === null) {
+      const parentItem = parent._item
+      if (parentItem === null) {
         // parent type on y._map
         // find the correct key
         const ykey = findRootTypeKey(parent)
@@ -530,7 +543,7 @@ export class Item extends AbstractStruct {
         encoding.writeVarString(encoder, ykey)
       } else {
         encoding.writeVarUint(encoder, 0) // write parent id
-        writeID(encoder, parent._item.id)
+        writeID(encoder, parentItem.id)
       }
       if (parentSub !== null) {
         encoding.writeVarString(encoder, parentSub)
@@ -723,22 +736,18 @@ export class ItemRef extends AbstractStructRef {
    */
   toStruct (transaction, store, offset) {
     if (offset > 0) {
-      /**
-       * @type {ID}
-       */
-      const id = this.id
-      this.id = createID(id.client, id.clock + offset)
+      this.id.clock += offset
       this.left = createID(this.id.client, this.id.clock - 1)
       this.content = this.content.splice(offset)
-      this.length -= offset
     }
 
     const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
     const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
+    const parentId = this.parent
     let parent = null
     let parentSub = this.parentSub
-    if (this.parent !== null) {
-      const parentItem = getItem(store, this.parent)
+    if (parentId !== null) {
+      const parentItem = getItem(store, parentId)
       // Edge case: toStruct is called with an offset > 0. In this case left is defined.
       // Depending in which order structs arrive, left may be GC'd and the parent not
       // deleted. This is why we check if left is GC'd. Strictly we don't have
@@ -767,9 +776,9 @@ export class ItemRef extends AbstractStructRef {
       : new Item(
         this.id,
         left,
-        this.left,
+        left && left.lastId,
         right,
-        this.right,
+        right && right.id,
         parent,
         parentSub,
         this.content
diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js
index b5014a99..8521d921 100644
--- a/src/types/AbstractType.js
+++ b/src/types/AbstractType.js
@@ -4,14 +4,14 @@ import {
   callEventHandlerListeners,
   addEventHandlerListener,
   createEventHandler,
-  nextID,
+  getState,
   isVisible,
   ContentType,
+  createID,
   ContentAny,
   ContentBinary,
-  createID,
   getItemCleanStart,
-  Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
+  ID, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
 } from '../internals.js'
 
 import * as map from 'lib0/map.js'
@@ -375,6 +375,9 @@ export const typeListGet = (type, index) => {
  */
 export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
   let left = referenceItem
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
+  const store = doc.store
   const right = referenceItem === null ? parent._start : referenceItem.right
   /**
    * @type {Array<Object|Array<any>|number>}
@@ -382,7 +385,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
   let jsonContent = []
   const packJsonContent = () => {
     if (jsonContent.length > 0) {
-      left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentAny(jsonContent))
+      left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
       left.integrate(transaction)
       jsonContent = []
     }
@@ -401,12 +404,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
         switch (c.constructor) {
           case Uint8Array:
           case ArrayBuffer:
-            left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
+            left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
             left.integrate(transaction)
             break
           default:
             if (c instanceof AbstractType) {
-              left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentType(c))
+              left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
               left.integrate(transaction)
             } else {
               throw new Error('Unexpected content type in insert operation')
@@ -509,6 +512,8 @@ export const typeMapDelete = (transaction, parent, key) => {
  */
 export const typeMapSet = (transaction, parent, key, value) => {
   const left = parent._map.get(key) || null
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
   let content
   if (value == null) {
     content = new ContentAny([value])
@@ -532,7 +537,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
         }
     }
   }
-  new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction)
+  new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction)
 }
 
 /**
diff --git a/src/types/YText.js b/src/types/YText.js
index e02b2514..5606e9bc 100644
--- a/src/types/YText.js
+++ b/src/types/YText.js
@@ -6,10 +6,10 @@
 import {
   YEvent,
   AbstractType,
-  nextID,
-  createID,
   getItemCleanStart,
+  getState,
   isVisible,
+  createID,
   YTextRefID,
   callTypeObservers,
   transact,
@@ -150,8 +150,10 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
     left = right
     right = right.right
   }
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
   for (const [key, val] of negatedAttributes) {
-    left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
+    left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
     left.integrate(transaction)
   }
   return { left, right }
@@ -215,6 +217,8 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
  * @function
  **/
 const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
   const negatedAttributes = new Map()
   // insert format-start items
   for (const key in attributes) {
@@ -223,7 +227,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
     if (!equalAttrs(currentVal, val)) {
       // save negated attribute (set null if currentVal undefined)
       negatedAttributes.set(key, currentVal)
-      left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
+      left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
       left.integrate(transaction)
     }
   }
@@ -249,13 +253,15 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
       attributes[key] = null
     }
   }
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
   const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
   const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
   left = insertPos.left
   right = insertPos.right
   // insert content
   const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
-  left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
+  left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
   left.integrate(transaction)
   return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
 }
@@ -274,6 +280,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
  * @function
  */
 const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
+  const doc = transaction.doc
+  const ownClientId = doc.clientID
   const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
   const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
   const negatedAttributes = insertPos.negatedAttributes
@@ -318,7 +326,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
     for (; length > 0; length--) {
       newlines += '\n'
     }
-    left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentString(newlines))
+    left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentString(newlines))
     left.integrate(transaction)
   }
   return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js
index dfcb31ac..943fa031 100644
--- a/src/utils/DeleteSet.js
+++ b/src/utils/DeleteSet.js
@@ -1,11 +1,11 @@
 
 import {
   findIndexSS,
-  createID,
   getState,
   splitItem,
+  createID,
   iterateStructs,
-  Item, GC, StructStore, Transaction, ID // eslint-disable-line
+  Item, AbstractStruct, GC, StructStore, Transaction, ID // eslint-disable-line
 } from '../internals.js'
 
 import * as array from 'lib0/array.js'
diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js
index 47335069..36b6e5b3 100644
--- a/src/utils/RelativePosition.js
+++ b/src/utils/RelativePosition.js
@@ -1,12 +1,12 @@
 
 import {
-  createID,
   writeID,
   readID,
   compareIDs,
   getState,
   findRootTypeKey,
   Item,
+  createID,
   ContentType,
   followRedone,
   ID, Doc, AbstractType // eslint-disable-line
@@ -107,7 +107,7 @@ export const createRelativePosition = (type, item) => {
   if (type._item === null) {
     tname = findRootTypeKey(type)
   } else {
-    typeid = type._item.id
+    typeid = createID(type._item.id.client, type._item.id.clock)
   }
   return new RelativePosition(typeid, tname, item)
 }
diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js
index 521d3c44..1c13cf6f 100644
--- a/src/utils/Snapshot.js
+++ b/src/utils/Snapshot.js
@@ -4,13 +4,13 @@ import {
   createDeleteSetFromStructStore,
   getStateVector,
   getItemCleanStart,
-  createID,
   iterateDeletedStructs,
   writeDeleteSet,
   writeStateVector,
   readDeleteSet,
   readStateVector,
   createDeleteSet,
+  createID,
   getState,
   Transaction, Doc, DeleteSet, Item // eslint-disable-line
 } from '../internals.js'
diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js
index 1ea11dd4..59b88dd5 100644
--- a/src/utils/StructStore.js
+++ b/src/utils/StructStore.js
@@ -2,7 +2,7 @@
 import {
   GC,
   splitItem,
-  GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
+  AbstractStruct, GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
 } from '../internals.js'
 
 import * as math from 'lib0/math.js'
@@ -114,7 +114,7 @@ export const addStruct = (store, struct) => {
 
 /**
  * Perform a binary search on a sorted array
- * @param {Array<any>} structs
+ * @param {Array<Item|GC>} structs
  * @param {number} clock
  * @return {number}
  *
@@ -163,16 +163,10 @@ export const find = (store, id) => {
 
 /**
  * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
- *
- * @param {StructStore} store
- * @param {ID} id
- * @return {Item}
- *
  * @private
  * @function
  */
-// @ts-ignore
-export const getItem = (store, id) => find(store, id)
+export const getItem = /** @type {function(StructStore,ID):Item} */ (find)
 
 /**
  * @param {Transaction} transaction
diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js
index 2fd0d598..be4a4ca4 100644
--- a/src/utils/Transaction.js
+++ b/src/utils/Transaction.js
@@ -1,7 +1,6 @@
 
 import {
   getState,
-  createID,
   writeStructsFromTransaction,
   writeDeleteSet,
   DeleteSet,
@@ -11,7 +10,8 @@ import {
   callEventHandlerListeners,
   Item,
   generateNewClientId,
-  StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
+  createID,
+  GC, StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
 } from '../internals.js'
 
 import * as encoding from 'lib0/encoding.js'
@@ -86,9 +86,9 @@ export class Transaction {
      */
     this.changedParentTypes = new Map()
     /**
-     * @type {Set<ID>}
+     * @type {Array<AbstractStruct>}
      */
-    this._mergeStructs = new Set()
+    this._mergeStructs = []
     /**
      * @type {any}
      */
@@ -170,7 +170,7 @@ const tryToMergeWithLeft = (structs, pos) => {
  */
 const tryGcDeleteSet = (ds, store, gcFilter) => {
   for (const [client, deleteItems] of ds.clients) {
-    const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
+    const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
     for (let di = deleteItems.length - 1; di >= 0; di--) {
       const deleteItem = deleteItems[di]
       const endDeleteItemClock = deleteItem.clock + deleteItem.len
@@ -199,7 +199,7 @@ const tryMergeDeleteSet = (ds, store) => {
   // try to merge deleted / gc'd items
   // merge from right to left for better efficiecy and so we don't miss any merge targets
   for (const [client, deleteItems] of ds.clients) {
-    const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
+    const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
     for (let di = deleteItems.length - 1; di >= 0; di--) {
       const deleteItem = deleteItems[di]
       // start with merging the item next to the last deleted item
@@ -235,6 +235,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
     const doc = transaction.doc
     const store = doc.store
     const ds = transaction.deleteSet
+    const mergeStructs = transaction._mergeStructs
     try {
       sortAndMergeDeleteSet(ds)
       transaction.afterState = getStateVector(transaction.doc.store)
@@ -292,7 +293,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
       for (const [client, clock] of transaction.afterState) {
         const beforeClock = transaction.beforeState.get(client) || 0
         if (beforeClock !== clock) {
-          const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
+          const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
           // we iterate from right to left so we can safely remove entries
           const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
           for (let i = structs.length - 1; i >= firstChangePos; i--) {
@@ -303,10 +304,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
       // try to merge mergeStructs
       // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
       //        but at the moment DS does not handle duplicates
-      for (const mid of transaction._mergeStructs) {
-        const client = mid.client
-        const clock = mid.clock
-        const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
+      for (let i = 0; i < mergeStructs.length; i++) {
+        const { client, clock } = mergeStructs[i].id
+        const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
         const replacedStructPos = findIndexSS(structs, clock)
         if (replacedStructPos + 1 < structs.length) {
           tryToMergeWithLeft(structs, replacedStructPos + 1)
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js
index 4c9717b9..cbbda630 100644
--- a/src/utils/UndoManager.js
+++ b/src/utils/UndoManager.js
@@ -3,14 +3,14 @@ import {
   iterateDeletedStructs,
   keepItem,
   transact,
+  createID,
   redoItem,
   iterateStructs,
   isParentOf,
-  createID,
   followRedone,
   getItemCleanStart,
   getState,
-  Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
+  ID, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
 } from '../internals.js'
 
 import * as time from 'lib0/time.js'
diff --git a/src/utils/encoding.js b/src/utils/encoding.js
index 00aee8ac..f85a5a00 100644
--- a/src/utils/encoding.js
+++ b/src/utils/encoding.js
@@ -19,15 +19,15 @@ import {
   GCRef,
   ItemRef,
   writeID,
-  createID,
   readID,
   getState,
+  createID,
   getStateVector,
   readAndApplyDeleteSet,
   writeDeleteSet,
   createDeleteSetFromStructStore,
   transact,
-  Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
+  Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
 } from '../internals.js'
 
 import * as encoding from 'lib0/encoding.js'
@@ -36,7 +36,7 @@ import * as binary from 'lib0/binary.js'
 
 /**
  * @param {encoding.Encoder} encoder
- * @param {Array<AbstractStruct>} structs All structs by `client`
+ * @param {Array<GC|Item>} structs All structs by `client`
  * @param {number} client
  * @param {number} clock write structs starting with `ID(client,clock)`
  *
@@ -50,35 +50,12 @@ const writeStructs = (encoder, structs, client, clock) => {
   writeID(encoder, createID(client, clock))
   const firstStruct = structs[startNewStructs]
   // write first struct with an offset
-  firstStruct.write(encoder, clock - firstStruct.id.clock, 0)
+  firstStruct.write(encoder, clock - firstStruct.id.clock)
   for (let i = startNewStructs + 1; i < structs.length; i++) {
-    structs[i].write(encoder, 0, 0)
+    structs[i].write(encoder, 0)
   }
 }
 
-/**
- * @param {decoding.Decoder} decoder
- * @param {number} numOfStructs
- * @param {ID} nextID
- * @return {Array<GCRef|ItemRef>}
- *
- * @private
- * @function
- */
-const readStructRefs = (decoder, numOfStructs, nextID) => {
-  /**
-   * @type {Array<GCRef|ItemRef>}
-   */
-  const refs = []
-  for (let i = 0; i < numOfStructs; i++) {
-    const info = decoding.readUint8(decoder)
-    const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, nextID, info) : new ItemRef(decoder, nextID, info)
-    nextID = createID(nextID.client, nextID.clock + ref.length)
-    refs.push(ref)
-  }
-  return refs
-}
-
 /**
  * @param {encoding.Encoder} encoder
  * @param {StructStore} store
@@ -111,22 +88,30 @@ export const writeClientsStructs = (encoder, store, _sm) => {
 
 /**
  * @param {decoding.Decoder} decoder The decoder object to read data from.
+ * @param {Map<number,Array<GCRef|ItemRef>>} clientRefs
  * @return {Map<number,Array<GCRef|ItemRef>>}
  *
  * @private
  * @function
  */
-export const readClientsStructRefs = decoder => {
-  /**
-   * @type {Map<number,Array<GCRef|ItemRef>>}
-   */
-  const clientRefs = new Map()
+export const readClientsStructRefs = (decoder, clientRefs) => {
   const numOfStateUpdates = decoding.readVarUint(decoder)
   for (let i = 0; i < numOfStateUpdates; i++) {
     const numberOfStructs = decoding.readVarUint(decoder)
     const nextID = readID(decoder)
-    const refs = readStructRefs(decoder, numberOfStructs, nextID)
-    clientRefs.set(nextID.client, refs)
+    const nextIdClient = nextID.client
+    let nextIdClock = nextID.clock
+    /**
+     * @type {Array<GCRef|ItemRef>}
+     */
+    const refs = []
+    clientRefs.set(nextIdClient, refs)
+    for (let i = 0; i < numberOfStructs; i++) {
+      const info = decoding.readUint8(decoder)
+      const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, createID(nextIdClient, nextIdClock), info) : new ItemRef(decoder, createID(nextIdClient, nextIdClock), info)
+      refs.push(ref)
+      nextIdClock += ref.length
+    }
   }
   return clientRefs
 }
@@ -171,16 +156,18 @@ const resumeStructIntegration = (transaction, store) => {
     }
     const ref = stack[stack.length - 1]
     const m = ref._missing
-    const client = ref.id.client
+    const refID = ref.id
+    const client = refID.client
+    const refClock = refID.clock
     const localClock = getState(store, client)
-    const offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0
-    if (ref.id.clock + offset !== localClock) {
+    const offset = refClock < localClock ? localClock - refClock : 0
+    if (refClock + offset !== localClock) {
       // A previous message from this client is missing
       // check if there is a pending structRef with a smaller clock and switch them
       const structRefs = clientsStructRefs.get(client)
       if (structRefs !== undefined) {
         const r = structRefs.refs[structRefs.i]
-        if (r.id.clock < ref.id.clock) {
+        if (r.id.clock < refClock) {
           // put ref with smaller clock on stack instead and continue
           structRefs.refs[structRefs.i] = ref
           stack[stack.length - 1] = r
@@ -282,7 +269,8 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
  * @function
  */
 export const readStructs = (decoder, transaction, store) => {
-  const clientsStructRefs = readClientsStructRefs(decoder)
+  const clientsStructRefs = new Map()
+  readClientsStructRefs(decoder, clientsStructRefs)
   mergeReadStructsIntoPendingReads(store, clientsStructRefs)
   resumeStructIntegration(transaction, store)
   tryResumePendingDeleteReaders(transaction, store)
diff --git a/tests/testHelper.js b/tests/testHelper.js
index 9d1acf42..3e669b5f 100644
--- a/tests/testHelper.js
+++ b/tests/testHelper.js
@@ -330,6 +330,7 @@ export const compareStructStores = (ss1, ss2) => {
         s1.constructor !== s2.constructor ||
         !Y.compareIDs(s1.id, s2.id) ||
         s1.deleted !== s2.deleted ||
+        // @ts-ignore
         s1.length !== s2.length
       ) {
         t.fail('Structs dont match')
diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js
index ff08615e..e388ba1f 100644
--- a/tests/y-text.tests.js
+++ b/tests/y-text.tests.js
@@ -205,6 +205,23 @@ export const testFormattingRemovedInMidText = tc => {
   t.assert(Y.getTypeChildren(text0).length === 3)
 }
 
+/**
+ * @param {t.TestCase} tc
+ *
+export const testLargeFragmentedDocument = tc => {
+  const { text0, text1, testConnector } = init(tc, { users: 2 })
+  // @ts-ignore
+  text0.doc.transact(() => {
+    for (let i = 0; i < 1000000; i++) {
+      text0.insert(0, '0')
+    }
+  })
+  t.measureTime('time to apply', () => {
+    testConnector.flushAllMessages()
+  })
+}
+*/
+
 // RANDOM TESTS
 
 let charCounter = 0