Skip to content

Commit

Permalink
Merge pull request #12 from azimgd/turbo
Browse files Browse the repository at this point in the history
Improve performance of initial rendering for large lists
  • Loading branch information
azimgd authored Jan 8, 2025
2 parents e42ddd8 + 458079e commit 27eb07a
Show file tree
Hide file tree
Showing 104 changed files with 28,709 additions and 5,621 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 azimgd
Copyright (c) 2025 azimgd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ It invokes Yoga for precise layout measurements of Shadow Nodes and constructs a
| No Sidebar Indicator Jump |||
| Native Bidirectional List |||
| Instant initialScrollIndex |||
| Instant initialScrollIndex |||
| Nested ShadowList (ScrollView) |||
| Natively Inverted List Support |||
| Smooth Scrolling |||
Expand All @@ -26,9 +25,8 @@ It invokes Yoga for precise layout measurements of Shadow Nodes and constructs a
> **FlashList is unreliable and completely breaks when scrolling, resulting in unrealistic metrics.*
> Given measurements show memory usage and FPS on fully loaded content, see demo [here](https://github.com/azimgd/shadowlist/issues/1) and implementation details [here](https://github.com/azimgd/shadowlist/blob/main/example/src/App.tsx).
## Note on Performance Considerations

ShadowList initiates ShadowNode creation for each child. This process can be slower when rendering a large number of items at once, which may impact performance compared to purely JS-based solutions. However, once the children are measured, it performs real-time virtualization ensuring smooth, flicker-free scrolling.
## Important Note
Shadowlist doesn't support state updates or dynamic prop calculations inside the renderItem function. Any changes to child components should be made through the data prop. This also applies to animations. This restriction will be addressed in future updates.

One temporary way to mitigate this is by implementing list pagination until the [following problem is addressed](https://github.com/reactwg/react-native-new-architecture/discussions/223).

Expand Down
12 changes: 5 additions & 7 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ android {
sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"generated/java",
"generated/jni"
]
java.srcDirs += [
"shadowlist/java",
"shadowlist/jni"
]
}
}
}
Expand All @@ -111,14 +111,12 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}

if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
libraryName = "ShadowlistView"
libraryName = "Shadowlist"
codegenJavaPackageName = "com.shadowlist"
}
}
29 changes: 29 additions & 0 deletions android/shadowlist/java/com/shadowlist/NativeShadowlistSpec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.shadowlist;

import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import com.facebook.jni.HybridData;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.common.UIManagerType;
import javax.annotation.Nonnull;

public abstract class NativeShadowlistSpec extends ReactContextBaseJavaModule implements TurboModule {
public static final String NAME = "Shadowlist";

public NativeShadowlistSpec(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public @Nonnull String getName() {
return NAME;
}

@ReactMethod
@DoNotStrip
public abstract void setup();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.shadowlist;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class SLContainerManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & SLContainerManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public SLContainerManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "data":
mViewManager.setData(view, value == null ? null : (String) value);
break;
case "inverted":
mViewManager.setInverted(view, value == null ? false : (boolean) value);
break;
case "horizontal":
mViewManager.setHorizontal(view, value == null ? false : (boolean) value);
break;
case "initialNumToRender":
mViewManager.setInitialNumToRender(view, value == null ? 0 : ((Double) value).intValue());
break;
case "initialScrollIndex":
mViewManager.setInitialScrollIndex(view, value == null ? 0 : ((Double) value).intValue());
break;
default:
super.setProperty(view, propName, value);
}
}

@Override
public void receiveCommand(T view, String commandName, @Nullable ReadableArray args) {
switch (commandName) {
case "scrollToIndex":
mViewManager.scrollToIndex(view, args.getInt(0), args.getBoolean(1));
break;
case "scrollToOffset":
mViewManager.scrollToOffset(view, args.getInt(0), args.getBoolean(1));
break;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.shadowlist;

import android.view.View;
import androidx.annotation.Nullable;

public interface SLContainerManagerInterface<T extends View> {
void setData(T view, @Nullable String value);
void setInverted(T view, boolean value);
void setHorizontal(T view, boolean value);
void setInitialNumToRender(T view, int value);
void setInitialScrollIndex(T view, int value);
void scrollToIndex(T view, int index, boolean animated);
void scrollToOffset(T view, int offset, boolean animated);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.shadowlist;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class SLElementManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & SLElementManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public SLElementManagerDelegate(U viewManager) {
super(viewManager);
}

@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "uniqueId":
mViewManager.setUniqueId(view, value == null ? null : (String) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.shadowlist;

import android.view.View;
import androidx.annotation.Nullable;

public interface SLElementManagerInterface<T extends View> {
void setUniqueId(T view, @Nullable String value);
}
57 changes: 57 additions & 0 deletions android/shadowlist/jni/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

set(LIB_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(LIB_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../cpp)

file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS
${LIB_JNI_DIR}/*.cpp
${LIB_JNI_DIR}/react/renderer/components/RNShadowlistSpec/*.cpp
${LIB_CPP_DIR}/react/renderer/components/RNShadowlistSpec/*.cpp
${LIB_CPP_DIR}/react/renderer/components/SLContainerSpec/*.cpp
${LIB_CPP_DIR}/react/renderer/components/SLElementSpec/*.cpp
)

file(GLOB LIB_INCLUDES_SRCS CONFIGURE_DEPENDS
${LIB_CPP_DIR}/fenwick/*.cpp
${LIB_CPP_DIR}/json/*.hpp
)

add_library(
react_codegen_RNShadowlistSpec
SHARED
${LIB_CODEGEN_SRCS}
${LIB_INCLUDES_SRCS}
)

target_include_directories(react_codegen_RNShadowlistSpec PUBLIC
${LIB_JNI_DIR}/
${LIB_CPP_DIR}/fenwick
${LIB_CPP_DIR}/json
${LIB_CPP_DIR}/react/renderer/components/SLContainerSpec
${LIB_CPP_DIR}/react/renderer/components/SLElementSpec
${LIB_CPP_DIR}/react/renderer/components/RNShadowlistSpec
${LIB_JNI_DIR}/react/renderer/components/RNShadowlistSpec
)

find_library(logger log)

target_link_libraries(
react_codegen_RNShadowlistSpec
fbjni
jsi
# We need to link different libraries based on whether we are building rncore or not, that's necessary
# because we want to break a circular dependency between react_codegen_rncore and reactnative
reactnative
${logger}
)

target_compile_options(
react_codegen_RNShadowlistSpec
PRIVATE
-DLOG_TAG=\"ReactNative\"
-fexceptions
-frtti
-std=c++20
-Wall
)
15 changes: 15 additions & 0 deletions android/shadowlist/jni/OnLoad.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include <fbjni/fbjni.h>

#include "ShadowlistModule.h"
#include "SLRuntimeManager.h"

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
facebook::react::ShadowlistModule::registerNatives();
});
}

extern "C" JNIEXPORT void JNICALL Java_com_shadowlist_ShadowlistModule_injectJSIBindings(JNIEnv *env, jobject thiz, jlong jsiRuntime) {
jsi::Runtime* runtime = reinterpret_cast<jsi::Runtime*>(jsiRuntime);
SLRuntimeManager::getInstance().setRuntime(runtime);
}
21 changes: 21 additions & 0 deletions android/shadowlist/jni/RNShadowlistSpec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include "RNShadowlistSpec.h"

namespace facebook::react {

static facebook::jsi::Value __hostFunction_NativeShadowlistSpecJSI_setup(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "setup", "()V", args, count, cachedMethodId);
}

NativeShadowlistSpecJSI::NativeShadowlistSpecJSI(const JavaTurboModule::InitParams &params): JavaTurboModule(params) {
methodMap_["setup"] = MethodMetadata {0, __hostFunction_NativeShadowlistSpecJSI_setup};
}

std::shared_ptr<TurboModule> RNShadowlistSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params) {
if (moduleName == "Shadowlist") {
return std::make_shared<NativeShadowlistSpecJSI>(params);
}
return nullptr;
}

}
22 changes: 22 additions & 0 deletions android/shadowlist/jni/RNShadowlistSpec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include "SLElementComponentDescriptor.h"
#include "SLContainerComponentDescriptor.h"

namespace facebook::react {

/**
* JNI C++ class for module 'NativeShadowlist'
*/
class JSI_EXPORT NativeShadowlistSpecJSI : public JavaTurboModule {
public:
NativeShadowlistSpecJSI(const JavaTurboModule::InitParams &params);
};

JSI_EXPORT
std::shared_ptr<TurboModule> RNShadowlistSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params);

}
35 changes: 35 additions & 0 deletions android/shadowlist/jni/ShadowlistModule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <react/fabric/Binding.h>
#include <react/renderer/scheduler/Scheduler.h>

#include "ShadowlistModule.h"
#include "SLModuleJSI.h"

namespace facebook::react {

using namespace facebook;
using namespace react;

ShadowlistModule::ShadowlistModule(jni::alias_ref<ShadowlistModule::javaobject> jThis): javaPart_(jni::make_global(jThis)) {}

ShadowlistModule::~ShadowlistModule() {}

void ShadowlistModule::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", ShadowlistModule::initHybrid),
makeNativeMethod("createCommitHook", ShadowlistModule::createCommitHook),
});
}

void ShadowlistModule::createCommitHook(jni::alias_ref<facebook::react::JFabricUIManager::javaobject> fabricUIManager) {
const auto &uiManager = fabricUIManager->getBinding()->getScheduler()->getUIManager();
commitHook_ = std::make_shared<SLCommitHook>(uiManager);

jni::make_local(BindingsInstallerHolder::newObjectCxxArgs([&](jsi::Runtime& runtime) {
}));
}

jni::local_ref<ShadowlistModule::jhybriddata> ShadowlistModule::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}

}
33 changes: 33 additions & 0 deletions android/shadowlist/jni/ShadowlistModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

#include <ReactCommon/BindingsInstallerHolder.h>
#include <react/fabric/JFabricUIManager.h>
#include <fbjni/fbjni.h>
#include "SLCommitHook.h"

#include <string>

namespace facebook::react {

using namespace facebook;
using namespace facebook::jni;

class ShadowlistModule : public jni::HybridClass<ShadowlistModule> {
public:
static auto constexpr kJavaDescriptor = "Lcom/shadowlist/ShadowlistModule;";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
static void registerNatives();

~ShadowlistModule();

private:
friend HybridBase;
jni::global_ref<ShadowlistModule::javaobject> javaPart_;
std::shared_ptr<SLCommitHook> commitHook_;

explicit ShadowlistModule(jni::alias_ref<ShadowlistModule::javaobject> jThis);

void createCommitHook(jni::alias_ref<facebook::react::JFabricUIManager::javaobject> fabricUIManager);
};

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
#include "ComponentDescriptors.h"
#include "SLContainerComponentDescriptor.h"
#include "SLElementComponentDescriptor.h"

#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook::react {

void SLContainerSpec_registerComponentDescriptorsFromCodegen(
void RNShadowlistSpec_registerComponentDescriptorsFromCodegen(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry) {
registry->add(concreteComponentDescriptorProvider<SLContainerComponentDescriptor>());
registry->add(concreteComponentDescriptorProvider<SLElementComponentDescriptor>());
}

}
Loading

0 comments on commit 27eb07a

Please sign in to comment.