VirtualizeUtils.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const invariant = require('invariant');
  12. /**
  13. * Used to find the indices of the frames that overlap the given offsets. Useful for finding the
  14. * items that bound different windows of content, such as the visible area or the buffered overscan
  15. * area.
  16. */
  17. function elementsThatOverlapOffsets(
  18. offsets: Array<number>,
  19. itemCount: number,
  20. getFrameMetrics: (
  21. index: number,
  22. ) => {
  23. length: number,
  24. offset: number,
  25. ...
  26. },
  27. ): Array<number> {
  28. const out = [];
  29. let outLength = 0;
  30. for (let ii = 0; ii < itemCount; ii++) {
  31. const frame = getFrameMetrics(ii);
  32. const trailingOffset = frame.offset + frame.length;
  33. for (let kk = 0; kk < offsets.length; kk++) {
  34. if (out[kk] == null && trailingOffset >= offsets[kk]) {
  35. out[kk] = ii;
  36. outLength++;
  37. if (kk === offsets.length - 1) {
  38. invariant(
  39. outLength === offsets.length,
  40. 'bad offsets input, should be in increasing order: %s',
  41. JSON.stringify(offsets),
  42. );
  43. return out;
  44. }
  45. }
  46. }
  47. }
  48. return out;
  49. }
  50. /**
  51. * Computes the number of elements in the `next` range that are new compared to the `prev` range.
  52. * Handy for calculating how many new items will be rendered when the render window changes so we
  53. * can restrict the number of new items render at once so that content can appear on the screen
  54. * faster.
  55. */
  56. function newRangeCount(
  57. prev: {
  58. first: number,
  59. last: number,
  60. ...
  61. },
  62. next: {
  63. first: number,
  64. last: number,
  65. ...
  66. },
  67. ): number {
  68. return (
  69. next.last -
  70. next.first +
  71. 1 -
  72. Math.max(
  73. 0,
  74. 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first),
  75. )
  76. );
  77. }
  78. /**
  79. * Custom logic for determining which items should be rendered given the current frame and scroll
  80. * metrics, as well as the previous render state. The algorithm may evolve over time, but generally
  81. * prioritizes the visible area first, then expands that with overscan regions ahead and behind,
  82. * biased in the direction of scroll.
  83. */
  84. function computeWindowedRenderLimits(
  85. props: {
  86. data: any,
  87. getItemCount: (data: any) => number,
  88. maxToRenderPerBatch: number,
  89. windowSize: number,
  90. ...
  91. },
  92. prev: {
  93. first: number,
  94. last: number,
  95. ...
  96. },
  97. getFrameMetricsApprox: (
  98. index: number,
  99. ) => {
  100. length: number,
  101. offset: number,
  102. ...
  103. },
  104. scrollMetrics: {
  105. dt: number,
  106. offset: number,
  107. velocity: number,
  108. visibleLength: number,
  109. ...
  110. },
  111. ): {
  112. first: number,
  113. last: number,
  114. ...
  115. } {
  116. const {data, getItemCount, maxToRenderPerBatch, windowSize} = props;
  117. const itemCount = getItemCount(data);
  118. if (itemCount === 0) {
  119. return prev;
  120. }
  121. const {offset, velocity, visibleLength} = scrollMetrics;
  122. // Start with visible area, then compute maximum overscan region by expanding from there, biased
  123. // in the direction of scroll. Total overscan area is capped, which should cap memory consumption
  124. // too.
  125. const visibleBegin = Math.max(0, offset);
  126. const visibleEnd = visibleBegin + visibleLength;
  127. const overscanLength = (windowSize - 1) * visibleLength;
  128. // Considering velocity seems to introduce more churn than it's worth.
  129. const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
  130. const fillPreference =
  131. velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none';
  132. const overscanBegin = Math.max(
  133. 0,
  134. visibleBegin - (1 - leadFactor) * overscanLength,
  135. );
  136. const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
  137. const lastItemOffset = getFrameMetricsApprox(itemCount - 1).offset;
  138. if (lastItemOffset < overscanBegin) {
  139. // Entire list is before our overscan window
  140. return {
  141. first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
  142. last: itemCount - 1,
  143. };
  144. }
  145. // Find the indices that correspond to the items at the render boundaries we're targeting.
  146. let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
  147. [overscanBegin, visibleBegin, visibleEnd, overscanEnd],
  148. props.getItemCount(props.data),
  149. getFrameMetricsApprox,
  150. );
  151. overscanFirst = overscanFirst == null ? 0 : overscanFirst;
  152. first = first == null ? Math.max(0, overscanFirst) : first;
  153. overscanLast = overscanLast == null ? itemCount - 1 : overscanLast;
  154. last =
  155. last == null
  156. ? Math.min(overscanLast, first + maxToRenderPerBatch - 1)
  157. : last;
  158. const visible = {first, last};
  159. // We want to limit the number of new cells we're rendering per batch so that we can fill the
  160. // content on the screen quickly. If we rendered the entire overscan window at once, the user
  161. // could be staring at white space for a long time waiting for a bunch of offscreen content to
  162. // render.
  163. let newCellCount = newRangeCount(prev, visible);
  164. while (true) {
  165. if (first <= overscanFirst && last >= overscanLast) {
  166. // If we fill the entire overscan range, we're done.
  167. break;
  168. }
  169. const maxNewCells = newCellCount >= maxToRenderPerBatch;
  170. const firstWillAddMore = first <= prev.first || first > prev.last;
  171. const firstShouldIncrement =
  172. first > overscanFirst && (!maxNewCells || !firstWillAddMore);
  173. const lastWillAddMore = last >= prev.last || last < prev.first;
  174. const lastShouldIncrement =
  175. last < overscanLast && (!maxNewCells || !lastWillAddMore);
  176. if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
  177. // We only want to stop if we've hit maxNewCells AND we cannot increment first or last
  178. // without rendering new items. This let's us preserve as many already rendered items as
  179. // possible, reducing render churn and keeping the rendered overscan range as large as
  180. // possible.
  181. break;
  182. }
  183. if (
  184. firstShouldIncrement &&
  185. !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)
  186. ) {
  187. if (firstWillAddMore) {
  188. newCellCount++;
  189. }
  190. first--;
  191. }
  192. if (
  193. lastShouldIncrement &&
  194. !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)
  195. ) {
  196. if (lastWillAddMore) {
  197. newCellCount++;
  198. }
  199. last++;
  200. }
  201. }
  202. if (
  203. !(
  204. last >= first &&
  205. first >= 0 &&
  206. last < itemCount &&
  207. first >= overscanFirst &&
  208. last <= overscanLast &&
  209. first <= visible.first &&
  210. last >= visible.last
  211. )
  212. ) {
  213. throw new Error(
  214. 'Bad window calculation ' +
  215. JSON.stringify({
  216. first,
  217. last,
  218. itemCount,
  219. overscanFirst,
  220. overscanLast,
  221. visible,
  222. }),
  223. );
  224. }
  225. return {first, last};
  226. }
  227. const VirtualizeUtils = {
  228. computeWindowedRenderLimits,
  229. elementsThatOverlapOffsets,
  230. newRangeCount,
  231. };
  232. module.exports = VirtualizeUtils;