I have a question regarding UI hang issues when using CompositionalLayout. As the number of cells displayed on the screen increases, the UI becomes less responsive. Could you provide advice or solutions for optimizing performance in such cases?

We are currently combining AutoLayout and StackView to structure our cells, and we’re using UICollectionViewCompositionalLayout to set up sections. Additionally, we’re utilizing UICollectionViewDiffableDataSource to manage the data source. On the home screen, we display cells in a multi-section layout.

In the main screen, we support infinite scrolling and use a paging method that fetches 10 items at a time from the API. As we fetch and render more data, the number of displayed cells increases. However, we’ve noticed that as the number of displayed cells grows, the UI freezes during the process of fetching and rendering more data.

We suspect that the issue lies in the configuration of UICollectionViewCompositionalLayout. When data is fetched through paging, the data is applied to the CollectionViewDiffableDataSource, and during the process of displaying it on the screen, the method that configures the UICollectionViewCompositionalLayout layout is called. The problem here is that when 10 cells are displayed on the screen and we fetch more data through paging and add 10 more cells, the layout calculation is redone for all 20 cells. In other words, the UICollectionViewCompositionalLayout layout configuration occurs a total of 20 times.

Because of this, it seems that the UI freezing issue occurs as the number of cells increases.

What steps can we take to resolve this problem? We have attached the relevant code for your reference.

	private lazy var collectionView = UICollectionView(
		frame: .zero,
		collectionViewLayout: self.createCollectionViewLayout()
	)

	private func createCollectionViewLayout() -> UICollectionViewLayout {
		let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
			guard let self, self.dataSource.snapshot().sectionIdentifiers.isEmpty == false
			else { return LayoutProvider.emptySection() }

private func applyRefreshSnapshot(with sections: [PlateViewSection]) {
		var snapshot = self.dataSource.snapshot()

		snapshot.deleteAllItems()
		snapshot.appendSections(sections)
		sections.forEach { section in
			snapshot.appendItems(section.items, toSection: section)
		}

		self.dataSource.apply(snapshot, animatingDifferences: false)
	}

			let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[safe: sectionIndex]
			let analyticsScreen = self.payload.analyticsScreen

			switch sectionIdentifier?.module {
			case let .tabOutlined(_, reactor):
				if self.tabOutlinedView == nil {
					let tabOutlinedView = self.dependency.tabOutlinedViewFactory.create(
						payload: .init(
							reactor: reactor,
							collectionView: self.collectionView,
							analyticsScreen: analyticsScreen,
							currentDisplayDepthObserver: .init { event in
								guard let depth = event.element else { return }

								self.currentLayoutHeaderTabDepth = depth
							},
							selectedTabItemObserver: .init { [weak self] event in
								guard let (tab, isPrimaryTabClick) = event.element else { return }

								var queryParams: [String: String]?

								if isPrimaryTabClick == false {
									guard let currentState = self?.reactor?.currentState else { return }
									let currentSelectedQueryParams = self?.dependency.userDefaults
										.dictionary(forKey: Keys.PlateParamsKeys.brandQueryParams) as? [String: String]

									queryParams = currentSelectedQueryParams ?? currentState.defaultQueryParams
								}

								self?.scrollCollectionViewToTop()
								self?.collectionView.viewWithTag(
									QueryToggleModuleCell.Metric.optionsMenuViewTag
								)?.removeFromSuperview()
								self?.reactor?.action.onNext(.refreshWithParams(
									tabParams: tab?.params,
									queryParams: queryParams
								))
							}
						)
					)

					self.view.addSubview(tabOutlinedView)

					tabOutlinedView.snp.makeConstraints { make in
						make.top.leading.trailing.equalToSuperview()
					}

					self.view.layoutIfNeeded()

					self.tabOutlinedView = tabOutlinedView
					self.tabOutlinedView?.emitInitializeAction()
				}

				return LayoutProvider.emptySection()

			case let .carouselOneRowBrand(module, _):
				let section = self.dependency.layoutProvider.createLayoutSection(module: module)
				section.visibleItemsInvalidationHandler = { [weak self] _, _, _ in
					guard let self else { return }

					self.emitFetchLikedAction(module: module, sectionIndex: sectionIndex)
				}

				return section

			case let .queryToggle(module):
				return self.dependency.layoutProvider.createLayoutSection(module: module)

			case .loadingIndicator:
				return LayoutProvider.loadingIndicatorSection()

			case let .space(module):
				return self.dependency.layoutProvider.createLayoutSection(module: module)

			case let .noResult(module):
				return self.dependency.layoutProvider.createLayoutSection(module: module)

			case let .buttonViewAll(module):
				return self.dependency.layoutProvider.createLayoutSection(module: module)

			case .footer:
				return LayoutProvider.footerSection()

			default:
				return LayoutProvider.emptySection()
			}
		}

		return layout
	}

private func applyAppendSnapshot(with sections: [PlateViewSection]) {
		var snapshot = self.dataSource.snapshot()

		// loadingIndicator section delete
		let sectionsToDelete = snapshot.sectionIdentifiers.filter { section in
			section.module == .loadingIndicator
		}
		snapshot.deleteSections(sectionsToDelete)

		snapshot.appendSections(sections)
		sections.forEach { section in
			snapshot.appendItems(section.items, toSection: section)
		}

		self.dataSource.apply(snapshot, animatingDifferences: false)
	}

Hi,

Try using Instruments with Time Profiler to narrow down what method(s) get slower as the number increases. Let's what results are yielded and go from there.

Rico

WWDR - DTS - Software Engineer

Hi,

We’ve already checked our project using Time Profiler, and it turns out that every time items or sections are added to the collectionView, the layout for all sections is recalculated.

For testing purposes, I’ve attached some simple code. When pressing the addSection or addItem buttons, instead of generating the layout only for the newly added sections and items through the createLayout method, the layout for all visible sections is being recreated.

This is causing performance issues. Is there any way to resolve this?

Is it the intended behavior of UICollectionViewCompositionalLayout to recalculate the layout for all sections and items whenever items are added, instead of checking the diff through NSDiffableDataSourceSnapshot and only generating the layout for the newly added sections and items?

Thanks!

I have a question regarding UI hang issues when using CompositionalLayout. As the number of cells displayed on the screen increases, the UI becomes less responsive. Could you provide advice or solutions for optimizing performance in such cases?
 
 
Q